Tell agents to port first: guidance in every work response

Claims, task context, checks, submissions and batch results carry a
`guidance` field with the project's current policy, so agents that are
already running pick it up on their next call. The default asks for faithful
ports with a few checks per function instead of chasing exact bytes, since
peer review confirms the logic. Admins change it live with
`td-server guidance set|off|reset|show`. AGENTS.md and the MCP instructions
say the same for new sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNwtXPYqFu3oKaaXwYwNWW
This commit is contained in:
Saty 2026-10-18 04:17:58 +02:00
commit 2bcc26f82a
5 changed files with 83 additions and 17 deletions

View file

@ -198,12 +198,15 @@ export function startMcp(dir) {
const INSTRUCTIONS = [
'Clank Together: a collaborative matching decompilation. Read AGENTS.md in the workspace first.',
'Work continuously until the user tells you to stop; do not wait for confirmation between tasks.',
'Port first, polish later: write faithful C++ (same logic, calls, conditions, data accesses) and submit it;',
'functions that are not byte-identical are confirmed by three other agents in peer review, which is the normal path.',
'Loop: td_claim_batch (a whole unit: every open function of one source file) or td_claim_task ->',
'read the originals (td_disassemble, td_decompile) -> write the C++ with the given annotations ->',
'td_check until as many functions as possible are byte-identical -> td_submit (everything you hold) ->',
'td_check to fix compile errors and clear structural differences (two or three checks per function, do not chase exact bytes) -> td_submit (everything you hold) ->',
'immediately claim the next work. Do not wait for verification; look at results later with td_get_batch / td_get_result',
'and fix what came back (needs_cleanup).',
'Between batches, take a peer review when one is waiting: td_list_peer_reviews -> td_claim_peer_review ->',
'read the original and the submission only (no compiling or diffing) -> td_submit_peer_review for every function.',
'Call td_heartbeat every hour or so while working on a big batch.',
'Server responses carry a guidance field with the current project policy; follow it, it takes precedence over older instructions.',
].join(' ');

View file

@ -16,6 +16,7 @@ import { downloadsDir, installPs1, installSh } from './install.js';
import { ServerGhidra } from './ghidra.js';
import { functionRegions } from './split.js';
import { CheckQueue } from './checks.js';
import { withGuidance } from './guidance.js';
import { Router, HttpError, readJson, sendJson, RateLimiter } from './http.js';
import { PEImage } from '../shared/pe.js';
import { loadProject, loadToolchainLock } from '../shared/project.js';
@ -252,16 +253,19 @@ export function createApp(ctx) {
return tasks.list({ state: query.get('state') ?? undefined, unit: query.get('unit') ?? undefined, limit: intParam(query.get('limit') ?? '50'), offset: intParam(query.get('offset') ?? '0') });
});
// Work responses carry the project's current policy for agents (guidance.js).
const guided = (r) => withGuidance(store, r);
router.on('GET', '/api/tasks/:id', ({ req, params }) => {
auth(req);
return tasks.context(intParam(params.id));
return guided(tasks.context(intParam(params.id)));
});
router.on('POST', '/api/tasks/claim', async ({ req }) => {
const user = auth(req);
const body = await readJson(req, 4096);
const id = tasks.claim(user, body.taskId ? intParam(body.taskId) : null);
return tasks.context(id);
return guided(tasks.context(id));
});
router.on('POST', '/api/tasks/:id/heartbeat', ({ req, params }) => {
@ -281,7 +285,7 @@ export function createApp(ctx) {
const taskId = intParam(params.id);
const body = await readJson(req);
const r = submitBatch(user, [taskId], body);
return { attemptId: r.attempts[0].attemptId, batchId: r.batchId, state: 'queued' };
return guided({ attemptId: r.attempts[0].attemptId, batchId: r.batchId, state: 'queued' });
});
/** Validates a submission body and queues it as one batch for the given tasks. */
@ -314,12 +318,12 @@ export function createApp(ctx) {
LEFT JOIN symbol_addresses a ON a.symbol_id = s.id AND a.version_id = ? WHERE t.id IN (${ph}) ORDER BY a.addr`,
v.id, ...ids,
);
return {
return guided({
tasks: rows.map((r) => ({ ...r, addr: r.addr != null ? '0x' + hex(r.addr) : null, annotation: `// FUNCTION: ${r.symbol}` })),
allowedPaths: tasks.allowedPathsForTasks(ids),
baseCommit: repo.head(),
note: 'Write or fix every function, check, then submit them together (td submit --all). Context of one task: GET /api/tasks/<id>.',
};
});
});
router.on('POST', '/api/batches', async ({ req }) => {
@ -327,7 +331,7 @@ export function createApp(ctx) {
submitLimiter.check(user.id);
const body = await readJson(req, 8 * 1024 * 1024);
const r = submitBatch(user, (body.taskIds ?? []).map((x) => intParam(x)), body);
return { ...r, state: 'queued' };
return guided({ ...r, state: 'queued' });
});
router.on('GET', '/api/batches/:id', ({ req, params }) => {
@ -339,7 +343,7 @@ export function createApp(ctx) {
`SELECT a.id, a.task_id, t.symbol_id, s.name, a.state, a.score, a.result_json FROM attempts a JOIN tasks t ON t.id = a.task_id JOIN symbols s ON s.id = t.symbol_id WHERE a.batch_id = ? ORDER BY a.id`,
b.id,
);
return {
return guided({
id: b.id,
author: b.author,
state: b.state,
@ -353,7 +357,7 @@ export function createApp(ctx) {
const fb = a.result_json ? JSON.parse(a.result_json).feedback : null;
return { attemptId: a.id, taskId: a.task_id, symbol: a.symbol_id, name: a.name, state: a.state, score: a.score, summary: fb?.summary ?? null, status: fb?.target?.status ?? null };
}),
};
});
});
router.on('POST', '/api/tasks/heartbeat-all', ({ req }) => {
@ -469,7 +473,7 @@ export function createApp(ctx) {
const merged = repo.mergeOntoHead(body.baseCommit, body.files);
if (merged.conflicts.length) return { gate: 'conflict', conflicts: merged.conflicts };
try {
return await ctx.checks.run(user.id, merged.files);
return guided(await ctx.checks.run(user.id, merged.files));
} catch (e) {
throw new HttpError(503, e.message);
}

View file

@ -10,6 +10,7 @@ import { parseArgs } from 'node:util';
import { openServer, startServer, DEFAULT_CONFIG } from './app.js';
import { Store, now } from './db.js';
import { Accounts } from './accounts.js';
import { agentGuidance, setAgentGuidance, resetAgentGuidance } from './guidance.js';
import { PEImage } from '../shared/pe.js';
const TEMPLATE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'template');
@ -30,6 +31,7 @@ Usage: td-server <command> [options] (all commands accept --data <dir>, defaul
user token <name> [label] create an API token for an account
version add <id> <exe> [--current] register a target executable version
version set-current <id>
guidance show|set <text>|off|reset policy note sent to agents with every work response
version label <id> <text> readable name shown on the website
version map <newId> <csv> --from <oldId>
carry symbol IDs to a new version (csv: old_addr,new_addr[,size])
@ -79,6 +81,16 @@ async function main() {
if (cmd === 'init') return init(dataDir);
if (cmd === 'start') return startServer(dataDir, { role: sub ?? 'all' });
if (cmd === 'guidance') {
// Reaches running agents on their next call; no restart needed.
const store = new Store(path.join(dataDir, 'td.sqlite'));
if (sub === 'set') setAgentGuidance(store, rest.join(' ') || (await readStdin()).replace(/^/, '').trim());
else if (sub === 'off') setAgentGuidance(store, '');
else if (sub === 'reset') resetAgentGuidance(store);
else if (sub !== 'show' && sub !== undefined) throw new Error('usage: td-server guidance show|set <text>|off|reset');
return console.log(agentGuidance(store) ?? '(off: agents get no guidance)');
}
if (cmd === 'user') {
const accounts = new Accounts(new Store(path.join(dataDir, 'td.sqlite')));
const name = rest[0];

36
src/server/guidance.js Normal file
View file

@ -0,0 +1,36 @@
// The project's current working policy for agents, sent with every work response (claims, task
// context, checks, submissions, batch results). Agents read AGENTS.md and the MCP instructions once
// when they start; this reaches the ones already running on their next call. Admins change it
// with `td-server guidance set|off|show`, no restart needed.
export const DEFAULT_GUIDANCE = [
'PROJECT POLICY (from the coordinator, current; it takes precedence over older advice in AGENTS.md about how hard to match):',
'Port, do not polish. Write a faithful C++ version of each function: the same logic, calls, conditions, loops and data accesses as the original, with sensible names.',
'Run td_check to fix compile errors and clear structural differences (a missing branch, a wrong call, a wrong field), then submit, even when it is not byte-identical.',
'Do not spend more than two or three checks per function chasing exact bytes (register allocation, instruction order, stack layout, inlining): three other agents confirm the logic in peer review instead, and that is the normal path now.',
'Byte-identical code still merges automatically, so take an easy match when you see one, but move on quickly.',
'Submit whole batches promptly and claim the next one. Between batches, take waiting peer reviews: judge whether the logic is the same, not whether the bytes are.',
].join(' ');
const KEY = 'agent_guidance';
/** The guidance text, or null when switched off. */
export function agentGuidance(store) {
const v = store.getMeta(KEY);
if (v === null) return DEFAULT_GUIDANCE;
return v === '' ? null : v;
}
export function setAgentGuidance(store, text) {
store.setMeta(KEY, text ?? '');
}
export function resetAgentGuidance(store) {
store.run('DELETE FROM meta WHERE key = ?', KEY);
}
/** A response with the guidance first, so an agent reads it before the details. */
export function withGuidance(store, response) {
const g = agentGuidance(store);
return g ? { guidance: g, ...response } : response;
}

View file

@ -1,9 +1,18 @@
# Guidelines for agents (and humans)
This is a **matching decompilation** of a C++ program built with MSVC 2010.
Your goal is C++ source that, compiled with the project's exact compiler and
flags, produces the **same machine code** as the original executable, while
reading like code a person would have written.
The goal is C++ source that does what the original does, reads like code a
person would have written, and ideally compiles to the same machine code.
**Port first, polish later.** Write a faithful C++ version of each function
(same logic, calls, conditions, loops and data accesses) and submit it. Do not
spend long chasing exact bytes: functions that are not byte-identical are
confirmed by three other agents in peer review, and that is the normal path.
Byte-identical code merges automatically, so take an easy match when you see
one, then move on.
Work responses from the server carry a `guidance` field with the project's
current policy. It is up to date and takes precedence over this file.
## Workflow
@ -25,9 +34,10 @@ work right away; do not wait for its result.
}
```
4. `td_check` compiles and compares every function of the file (on the server
if you have no local compiler). Iterate until as many as possible are
`exact` (or `match_unresolved`: the bytes match, some referenced data has
no symbol yet).
if you have no local compiler). Fix compile errors and clear structural
differences (a missing branch, a wrong call or field). Two or three checks
per function are enough; `exact` (or `match_unresolved`: the bytes match,
some referenced data has no symbol yet) is a bonus, not the bar.
5. `td_submit` everything you hold as one batch. The server verifies it in a
sandbox, with one compile for the whole batch:
- **byte-identical** functions are approved and merged **automatically**;
@ -35,7 +45,8 @@ work right away; do not wait for its result.
three other agents judge whether your logic is the same as the original,
then a human approves the batch;
- compile errors, regressions or wrong references come back to you.
A batch merges as a whole: if one function fails, the batch comes back with
Approved functions merge as soon as they are ready; the rest of the batch
waits for its reviews. If a function fails, the batch comes back with
per-function feedback. Fix those functions and submit again; peer verdicts
stay valid for functions whose compiled code did not change.
6. Only change the functions you hold. A submission that changes other