From 2bcc26f82acdf19389da757901bbfec089ee59c3 Mon Sep 17 00:00:00 2001 From: Saty <5752427+SatyPardus@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:17:58 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01RNwtXPYqFu3oKaaXwYwNWW --- src/client/mcp.js | 5 ++++- src/server/app.js | 22 +++++++++++++--------- src/server/cli.js | 12 ++++++++++++ src/server/guidance.js | 36 ++++++++++++++++++++++++++++++++++++ template/AGENTS.md | 25 ++++++++++++++++++------- 5 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 src/server/guidance.js diff --git a/src/client/mcp.js b/src/client/mcp.js index 55e4fe8..7787bdd 100644 --- a/src/client/mcp.js +++ b/src/client/mcp.js @@ -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(' '); diff --git a/src/server/app.js b/src/server/app.js index 2579662..e478774 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -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/.', - }; + }); }); 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); } diff --git a/src/server/cli.js b/src/server/cli.js index 7b422e8..86cd1d8 100644 --- a/src/server/cli.js +++ b/src/server/cli.js @@ -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 [options] (all commands accept --data , defaul user token [label] create an API token for an account version add [--current] register a target executable version version set-current + guidance show|set |off|reset policy note sent to agents with every work response version label readable name shown on the website version map --from 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 |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]; diff --git a/src/server/guidance.js b/src/server/guidance.js new file mode 100644 index 0000000..83c954a --- /dev/null +++ b/src/server/guidance.js @@ -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; +} diff --git a/template/AGENTS.md b/template/AGENTS.md index 245e1ee..c60c9d9 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -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