review loop: working repositories, git socket, push and review verdicts, row 93 (#1587)

Commits move as v2 bundles over bus-host/git.sock (decision 135). The bus
host keeps one bare working repository per business and project, seeds main
from the project root under a per-repository lock, fetches a pushed bundle
with fsck into a temp ref, requires history shared with main, and moves the
task's work ref only as a fast-forward. Host and runner git run with no
system or global config, hooks at /dev/null and local transports only. The
socket binds only for roles holding git.push.working or review.verdict.

The broker records a push as the action.allowed for git.push.working with
its commit, takes review.request for the latest push and one review.verdict
per request, and refuses task.close on a pushed task without an approve
verdict for the latest push. recordEvent reserves both shapes, so agents
can't forge either.

Reviewed by Dewey (28196) and Sage (28197), both approve. Gate on 7a729aef:
14 node suites and 9 scripts green.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 10:48:34 -05:00
co-authored by Claude Opus 5.5
parent 7a729aefa3
commit 4da6815293
29 changed files with 2149 additions and 55 deletions
+4 -2
View File
@@ -159,7 +159,8 @@ export function verbs(ctx) {
if (before.task.done) fail('task-done');
const work = plan.check(before, at);
await located(plan.others ?? []);
ctx.broker.authorize(cap, verb, { decision: plan.decision, target: args.task_ref });
// Row 93 (#1587): task.close's verdict goes to the broker, which checks it on a pushed task.
ctx.broker.authorize(cap, verb, { decision: plan.decision, target: args.task_ref, ...(verb === 'task.close' ? { verdict: args.verdict } : {}) });
let failure = null;
let landed = 0;
try {
@@ -363,7 +364,8 @@ export function verbs(ctx) {
}
case 'task.close': {
keys(args, [...common, 'verdict'], ['task_ref', 'verdict']);
// The queue holds review verdicts in slice 1. This is a citation, recorded and not checked.
// A review.verdict event id. On a task with a recorded push the broker checks it
// (row 93); on any other task it is a citation, recorded and not checked.
const verdict = text(args.verdict, 256, 1);
plan.check = (_before, { id, project, view }) => ({
steps: [
+32
View File
@@ -237,6 +237,38 @@ test('task.close needs a verdict; after it every verb refuses with task-done', a
await assert.rejects(w.call(coder, 'task.update.assigned', { task_ref, percent_done: 1 }), refusedWith('task-done'));
await assert.rejects(w.call(pm, 'task.close', { task_ref, verdict: 'again' }), refusedWith('task-done'));
});
test('a task with pushed work closes only citing an approve verdict for its latest push, and a refusal writes nothing', async (t) => {
// Row 93 (#1587): the verb hands its verdict to the broker's check.
const w = await ready(t, { business: (b) => (b.projects = { stack: { root: '/nonexistent' } }) });
const pm = w.agent('pm');
const coder = w.agent('coder');
const reviewer = w.agent('reviewer');
const { task_ref } = await task(w);
const push = (commit, previous) => {
w.broker.checkPush(coder, task_ref);
w.broker.recordPush(coder, { task_ref, project: 'stack', branch: 'feat', ref: 'work/vikunja-1-1', commit, previous });
};
const verdict = (commit, v) => {
const r = w.broker.request(coder, { verb: 'review.request', args: { task_ref, commit, to: 'reviewer' } });
return w.broker.request(reviewer, { verb: 'review.verdict', args: { task_ref, request: r.request, verdict: v, body: v } }).verdict;
};
const A = 'a'.repeat(40);
const B = 'b'.repeat(40);
push(A, null);
const writes = () => w.fake.requests.filter((r) => r.method !== 'GET').length;
const before = writes();
await assert.rejects(w.call(pm, 'task.close', { task_ref, verdict: 'queue:38' }), refusedWith('verdict-not-found'));
await assert.rejects(w.call(pm, 'task.close', { task_ref, verdict: verdict(A, 'changes') }), refusedWith('verdict-not-approve'));
const stale = verdict(A, 'approve');
push(B, A);
await assert.rejects(w.call(pm, 'task.close', { task_ref, verdict: stale }), refusedWith('verdict-stale'));
assert.equal(writes(), before, 'no refused close reached the tracker');
assert.equal(w.fake.task(1).done, false);
const fresh = verdict(B, 'approve');
await w.call(pm, 'task.close', { task_ref, verdict: fresh });
assert.equal(w.fake.task(1).done, true);
assert.deepEqual(w.events('task.closed')[0].body, { verdict: fresh });
});
test('a lost answer is settled by a re-read and never retried', async (t) => {
const w = await ready(t);
const pm = w.agent('pm');