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:
parent
2ec1bda2cc
commit
2bcc26f82a
5 changed files with 83 additions and 17 deletions
|
|
@ -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(' ');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
36
src/server/guidance.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue