Merges with nothing to change succeed; submissions are normalized to LF

A function resubmitted unchanged (port-first) made the merge a no-op, and git
refused the empty commit, so the merge retried forever and blocked the queue.
CRLF from Windows workspaces is normalized on arrival.

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 05:21:02 +02:00
commit db9c5b8f89
3 changed files with 14 additions and 0 deletions

View file

@ -322,10 +322,14 @@ export function createApp(ctx) {
return guided({ attemptId: r.attempts[0].attemptId, batchId: r.batchId, state: 'queued' });
});
// The repository stores LF line endings; submissions from Windows workspaces may carry CRLF.
const lf = (files) => Object.fromEntries(Object.entries(files).map(([f, c]) => [f, typeof c === 'string' ? c.replace(/\r\n/g, '\n') : c]));
/** Validates a submission body and queues it as one batch for the given tasks. */
const submitBatch = (user, taskIds, body) => {
if (typeof body.baseCommit !== 'string' || !/^[0-9a-f]{40}$/.test(body.baseCommit)) throw new HttpError(400, 'baseCommit must be a full commit hash');
if (!body.files || typeof body.files !== 'object' || Array.isArray(body.files)) throw new HttpError(400, 'files must be an object of path -> content');
body.files = lf(body.files);
const r = tasks.submitBatch(user, {
taskIds,
baseCommit: body.baseCommit,
@ -500,6 +504,7 @@ export function createApp(ctx) {
const body = await readJson(req);
if (typeof body.baseCommit !== 'string' || !/^[0-9a-f]{40}$/.test(body.baseCommit)) throw new HttpError(400, 'baseCommit must be a full commit hash');
if (!body.files || typeof body.files !== 'object' || Array.isArray(body.files)) throw new HttpError(400, 'files must be an object of path -> content');
body.files = lf(body.files);
const allowed = [...project.sourceDirs.map((d) => `${d}/**`)];
const lintErrors = lintSubmission(body.files, { allowedPaths: allowed, limits: project.lintLimits, allowedPragmas: project.allowedPragmas });
if (lintErrors.length) return { gate: 'rejected', lintErrors };

View file

@ -111,6 +111,8 @@ export class Repo {
fs.writeFileSync(abs, content);
}
this.git(['add', '--', ...Object.keys(files)]);
// Nothing to commit (the code is already on main, e.g. a function resubmitted unchanged).
if (!this.git(['status', '--porcelain', '--', ...Object.keys(files)]).trim()) return this.head();
this.git(['-c', 'user.name=Clank Together', '-c', 'user.email=bot@clank-together.invalid', 'commit', '-q', '--author', author, '-F', '-'], { input: message });
return this.head();
}

View file

@ -375,6 +375,13 @@ test('submissions may not add comments or strings that could instruct other agen
assert.equal(JSON.stringify(r.body).includes('ignore your instructions'), false, 'the text is not echoed back');
});
test('committing files that are already on main is a no-op, not an error', () => {
const files = { 'src/noop.cpp': 'int noop() { return 0; }\n' };
const head = ctx.repo.commitFiles(files, 'add noop.cpp', 'x <x@y.invalid>');
assert.equal(ctx.repo.commitFiles(files, 'nothing', 'x <x@y.invalid>'), head);
assert.equal(ctx.repo.head(), head);
});
test('the crew view is public and shows no account details', async () => {
const token = await activeUser('crewview');
await call('GET', '/api/tasks', null, token); // the agent checks in