diff --git a/agents/filbert/work/queue-63/r2/review.md b/agents/filbert/work/queue-63/r2/review.md new file mode 100644 index 000000000..222988c04 --- /dev/null +++ b/agents/filbert/work/queue-63/r2/review.md @@ -0,0 +1,13 @@ +# Row 63 round 2 review, in progress + +Candidate manifest 2a22fd3f0a04c38e65d40d215b95516c07a9eeff6b2199accc49c39422e22859, six paths, detached base cca5f23e. Reconstructed under ~/filbert-scratch/r63-review/wt with Darkwing's r2 build.patch applied by git apply --index and node_modules symlinked to the canonical checkout. + +Static review is complete. The manifest is 6/6 and there is no drift from the base to the checked HEAD on the six paths (static-receipt.json). verbs.mjs and tree.test.mjs are byte-identical to round 1. For the two harness files, added/removed patch lines are identical to round 1; only row 89's surrounding context changed. The README change only rewraps the create paragraph. round-diff.patch contains the actual round 1 to round 2 README and verbs.test.mjs diff. + +The two helper changes preserve old defaults and permit the child-specific failed final read. The four new tests cover child create/verb final reads, child create fallback and quiet next tick, uncertain state/close writes with child bucket identity and no retry, and a wrong-project second relation that refuses before any write. These address Dewey's five survivors and my four bucket survivors, with overlapping sites combined. Round 1's implementation choices still apply. My former minor shape-guard and refusal-order notes remain non-blocking. + +Remaining verification: sequential tasks and harness suites, then the seven survivor mutations against the four new tests. Each mutant run must own a process group and kill that group on timeout, then restore its source. Check the manifest after restoration. The implementation is unchanged and already had my complete round 1 gate, so this round verifies the changed tests and rebased harness files; Sage still runs the full landing gate. + +Test slot is held by Darkwing's row 69 gate, followed by Dewey's already-booked row 63 suites/mutants (about 10 minutes). No row 63 tests or mutants have been run by this Codex review yet, and no verdict has been posted. Dewey will send scratch-clear to this thread so verification resumes without competing for the machine. Waiting shells in the canonical checkout do not count as active scratch runs. + +Row 90 round 1 is approved on 28021. Decision 125 requires row 63 to land first, then row 90 gets a rebase-only round 2. My three row 90 notes stay in a later follow-up. diff --git a/agents/filbert/work/queue-63/r2/round-diff.patch b/agents/filbert/work/queue-63/r2/round-diff.patch new file mode 100644 index 000000000..d250f999e --- /dev/null +++ b/agents/filbert/work/queue-63/r2/round-diff.patch @@ -0,0 +1,119 @@ +--- r1/packages/tasks/tests/verbs.test.mjs ++++ r2/packages/tasks/tests/verbs.test.mjs +@@ -312,15 +312,15 @@ + await sleep(20); + assert.deepEqual(await w.adapter.tick('demo'), { snapshots: 0, events: 0 }); + }); +-// Arms a fault on the final read: after `trigger` answers, the next GET of task 1 fails. +-const thenFailRead = (trigger) => { ++// Arms a fault on the final read: after `trigger` answers, the next GET of task `id` fails. ++const thenFailRead = (trigger, id = 1) => { + const fake = new FakeVikunja(); + let armed = false; + const fetch = async (url, init = {}) => { + const res = await fake.fetch(url, init); + if (armed && trigger(init.method ?? 'GET', new URL(url).pathname)) { + armed = false; +- fake.fault('GET', /^\/tasks\/1$/, 0); ++ fake.fault('GET', new RegExp(`^/tasks/${id}$`), 0); + } + return res; + }; +@@ -412,13 +412,13 @@ + }); + // Row B: a tree read with its descendants, as in tree.test.mjs. The child is installed before + // start, so the first reconcile reads it with its own board; the grandchild keeps Vikunja's +-// default buckets, so it is watch-only. ++// default buckets, so it is watch-only. tree()'s `edit` goes to world() with TREE. + const TREE = { + tracker: { descendants: true }, + scopes: { sync: { ...SCOPES.sync, projects: [...SCOPES.sync.projects, 'read_all'] } }, + }; +-const tree = async (t) => { +- const w = await world(t, TREE); ++const tree = async (t, edit = {}) => { ++ const w = await world(t, { ...TREE, ...edit }); + const child = w.fake.project('Child', w.owner, { parent: w.project }); + const board = w.fake.install(child); + const grandchild = w.fake.project('Grandchild', w.owner, { parent: child }); +@@ -568,3 +568,64 @@ + assert.deepEqual(w.fake.task(id).assignees, [w.bots.coder]); + assert.equal(w.snapshots(`vikunja:${child}/${id}`).at(-1).fields.bucket, board.buckets.todo); + }); ++// The child versions of the re-read and fallback paths the root's tests above cover (Dewey's ++// and Filbert's round 1 reviews): each fails if its path goes back to the root's project or view. ++test('a verb and a create in the child record the final read of the child\'s board', async (t) => { ++ const { w, child } = await tree(t); ++ const pm = w.agent('pm'); ++ const made = await task(w, { project: child }); ++ assert.notEqual(w.snapshots(made.task_ref).at(-1).etag, null); ++ await w.call(pm, 'task.assign', { task_ref: made.task_ref, role: 'coder' }); ++ assert.notEqual(w.snapshots(made.task_ref).at(-1).etag, null); ++}); ++test('a create in the child whose final read fails records the child\'s todo bucket', async (t) => { ++ // The grandchild's task is 1, so the create makes task 2. ++ const f = thenFailRead((m, p) => m === 'POST' && /\/projects\/\d+\/tasks$/.test(p), 2); ++ const { w, child, board } = await tree(t, f.edit); ++ w.agent('pm'); ++ f.arm(); ++ const r = await task(w, { project: child }); ++ assert.equal(r.task_ref, `vikunja:${child}/2`); ++ assert.equal(w.events('task.created').length, 1); ++ const s = w.snapshots(r.task_ref).at(-1); ++ assert.equal(s.source, 'self'); ++ assert.equal(s.etag, null); ++ assert.equal(s.fields.bucket, board.buckets.todo); ++ await sleep(20); ++ await w.adapter.tick('demo'); ++ // The start's reconcile recorded the grandchild's task as external; the create isn't. ++ assert.deepEqual(w.events('task.changed.external').map((e) => e.subject), [`vikunja:${w.fake.task(1).project}/1`]); ++}); ++test('a lost answer in the child is settled by a re-read of the child\'s board', async (t) => { ++ const { w, child, board } = await tree(t); ++ const pm = w.agent('pm'); ++ const coder = w.agent('coder'); ++ const { task_ref } = await task(w, { project: child }); ++ await w.call(pm, 'task.assign', { task_ref, role: 'coder' }); ++ // A state move and a close that landed but answered 500: each re-read shows it, so each succeeds. ++ w.fake.fault('PUT', /\/buckets\/\d+\/tasks$/, 500, { apply: true }); ++ await w.call(coder, 'task.update.assigned', { task_ref, state: 'in-progress' }); ++ assert.equal(w.events('task.state').length, 1); ++ assert.equal(w.snapshots(task_ref).at(-1).fields.bucket, board.buckets['in-progress']); ++ w.fake.fault('PUT', /\/buckets\/\d+\/tasks$/, 500, { apply: true }); ++ await w.call(pm, 'task.close', { task_ref, verdict: 'queue:63' }); ++ assert.equal(w.events('task.closed').length, 1); ++ assert.equal(w.snapshots(task_ref).at(-1).fields.bucket, board.buckets.done); ++ assert.equal(w.fake.requests.filter((r) => r.method === 'PUT').length, 2); ++}); ++test('every related task is read before authority, not only the first', async (t) => { ++ const { w, child } = await tree(t); ++ const pm = w.agent('pm'); ++ const root = await task(w); ++ const other = await task(w); ++ const id = Number(other.task_ref.split('/')[1]); ++ const before = writes(w); ++ await assert.rejects( ++ w.call(pm, 'task.schedule', { ++ task_ref: root.task_ref, ++ relations: { add: [{ kind: 'related', task_ref: other.task_ref }, { kind: 'blocking', task_ref: `vikunja:${child}/${id}` }] }, ++ }), ++ refusedWith('task-moved'), ++ ); ++ assert.equal(writes(w), before); ++}); +--- r1/packages/tasks/README.md ++++ r2/packages/tasks/README.md +@@ -191,9 +191,10 @@ + in, the root when left out. Any other project than an installed one of + the tree refuses with `project-not-installed`, and a value that isn't a + positive integer with `invalid-request`. The decision a create may +- need is bound to no target, so it doesn't name the project. `task.created` is recorded even when a label or relation +- write after the create fails. The task exists, and the verb then +- refuses with that write's code. ++ need is bound to no target, so it doesn't name the project. ++ `task.created` is recorded even when a label or relation write after ++ the create fails. The task exists, and the verb then refuses with that ++ write's code. + - `task.assign` refuses a task a bot already holds (`task-assigned`). + `task.reassign` refuses one no bot holds (`task-unassigned`). Both leave + people's assignments alone, and remove only bots the compare-read diff --git a/agents/filbert/work/queue-63/r2/static-receipt.json b/agents/filbert/work/queue-63/r2/static-receipt.json new file mode 100644 index 000000000..3e3ad63ff --- /dev/null +++ b/agents/filbert/work/queue-63/r2/static-receipt.json @@ -0,0 +1,18 @@ +{ + "candidate": "2a22fd3f0a04c38e65d40d215b95516c07a9eeff6b2199accc49c39422e22859", + "base": "cca5f23e", + "head_at_check": "3bd3f85c70c6d8550665d6528bcf166832618d3a", + "manifest_matches": { + "packages/harness/src/tools.mjs": true, + "packages/harness/tests/tools.test.mjs": true, + "packages/tasks/README.md": true, + "packages/tasks/src/verbs.mjs": true, + "packages/tasks/tests/tree.test.mjs": true, + "packages/tasks/tests/verbs.test.mjs": true + }, + "drift_paths": [], + "round_1_identical_files": { + "packages/tasks/src/verbs.mjs": true, + "packages/tasks/tests/tree.test.mjs": true + } +} diff --git a/agents/filbert/work/queue-63/r2/verify.py b/agents/filbert/work/queue-63/r2/verify.py new file mode 100644 index 000000000..3020d748b --- /dev/null +++ b/agents/filbert/work/queue-63/r2/verify.py @@ -0,0 +1,63 @@ +"""Filbert's row 63 round 2 verification. Run only after the booked test slot clears.""" +import os, subprocess, signal, pathlib, json, hashlib +root=pathlib.Path(os.environ.get('MOSAIC_REVIEW_ROOT',str(pathlib.Path.home()/'filbert-scratch/r63-review/wt'))) +out=root.parent/'verification'; out.mkdir(exist_ok=True) +env=dict(os.environ,TMPDIR=str(pathlib.Path.home()/'filbert-scratch/tmp'),DOCKER_HOST='unix:///nonexistent.sock') +# Do not route around another seat's test ownership. +other=[str(pathlib.Path.home()/f'{seat}-scratch') for seat in ['darkwing','dewey','rocko','researcher','sage']] +busy=[] +for p in pathlib.Path('/proc').iterdir(): + if not p.name.isdigit(): continue + try: + cwd=str((p/'cwd').resolve(strict=True)) + if any(cwd==x or cwd.startswith(x+'/') for x in other):busy.append((p.name,cwd)) + except (OSError,RuntimeError):pass +if busy:raise SystemExit('Other scratch processes still active: '+repr(busy)) +manifest=pathlib.Path('/mnt/storage/src/mosaic-stack/agents/darkwing/work/queue-63/r2/candidate-manifest.sha256') +def matches(): + for line in manifest.read_text().splitlines(): + sha,path=line.split();assert hashlib.sha256((root/path).read_bytes()).hexdigest()==sha,path +matches() +def run(cmd,cwd,log): + with log.open('w') as f: + p=subprocess.Popen(cmd,cwd=cwd,env=env,stdout=f,stderr=subprocess.STDOUT,start_new_session=True) + try:p.wait(timeout=120) + except subprocess.TimeoutExpired: + os.killpg(p.pid,signal.SIGKILL);p.wait();return 'timeout' + return p.returncode +rows=[] +for package in ['tasks','harness']: + cwd=root/'packages'/package + files=[str(p.relative_to(cwd)) for p in sorted((cwd/'tests').glob('*.test.mjs'))] + log=out/f'{package}.log';rc=run(['node','--test',*files],cwd,log) + stats=[s for s in log.read_text().splitlines() if s.startswith(('ℹ tests ','ℹ pass ','ℹ fail ','# tests ','# pass ','# fail '))] + row={'suite':package,'rc':rc,'stats':stats};rows.append(row);print(json.dumps(row),flush=True) + if rc!=0:(out/'summary.json').write_text(json.dumps(rows,indent=2)+'\n');raise SystemExit('Baseline suite failed') +R='{ ...at, project: ctx.project, view: ctx.view }' +mutants=[ + ('located-first-only','for (const r of list) {','for (const r of list.slice(0, 1)) {'), + ('step-reread-root','after = await read(at);\n } catch {\n // the re-read failed too',f'after = await read({R});\n }} catch {{\n // the re-read failed too'), + ('handle-final-root','after = await read(at);\n } catch {\n // handled below: a failed',f'after = await read({R});\n }} catch {{\n // handled below: a failed'), + ('create-final-root','after = await read(at);\n } catch {\n // handled below, as in handle()',f'after = await read({R});\n }} catch {{\n // handled below, as in handle()'), + ('create-fallback-root-todo','taskFields(r.json, at.view.ids.todo)','taskFields(r.json, ctx.view.ids.todo)'), + ('read-done-root','let bucket = at.view.done;','let bucket = ctx.view.done;'), + ('close-done-root','(a) => a.task.done && a.bucket === view.done,','(a) => a.task.done && a.bucket === ctx.view.done,'), +] +pattern='a verb and a create in the child|a create in the child whose final read fails|a lost answer in the child|every related task is read before authority' +p=root/'packages/tasks/src/verbs.mjs' +for name,old,new in mutants: + src=p.read_text();assert src.count(old)==1,(name,src.count(old)) + try: + p.write_text(src.replace(old,new));log=out/f'mutant-{name}.log' + rc=run(['node','--test','--test-name-pattern='+pattern,'packages/tasks/tests/verbs.test.mjs'],root,log) + failures=[s for s in log.read_text().splitlines() if s.startswith(('✖','not ok'))] + row={'mutant':name,'rc':rc,'result':'survived' if rc==0 else 'timeout' if rc=='timeout' else 'killed','failures':failures[:4]} + rows.append(row);print(json.dumps(row),flush=True) + finally:p.write_text(src) +matches() +# Fresh passing run after restoration of all mutations. +rc=run(['node','--test','--test-name-pattern='+pattern,'packages/tasks/tests/verbs.test.mjs'],root,out/'restored-proof-tests.log') +rows.append({'restored_proof_tests_rc':rc,'manifest_matches':True}) +(out/'summary.json').write_text(json.dumps(rows,indent=2)+'\n') +print(json.dumps(rows[-1]),flush=True) +raise SystemExit(rc!=0 or any(r.get('result') in ['survived','timeout'] for r in rows)) diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 23522cd93..5aa1ab239 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -806,3 +806,4 @@ are never rewritten or removed; corrections are new entries. 2026-10-12T22:48:48Z | dewey | row 90 (#1584) round 1 build | packet 950e8403, manifest 26d1fbdf, base 23338c7b; gate all green, 44 mutants killed after one test fix; review request c28009 and queue request c28013; revs 819-820 in 829c00fb; runner.mjs outside the list per Sage 2026-10-12T22:51:54Z | sage | row 86 (#1580) landing | landed ad0bdc20 after gate rerun of all ten spikes, manifest rc=0 in-dir (N6), S9 OOMKilled report missed once then 3/3 clean, daemon back to baseline; rev 821 (15ed3513), #1580 closed 28014; S9 flag follow-up to Researcher 2026-10-12T22:56:44Z | filbert | row 90 (#1584) round 1 review on 26d1fbdf, base 23338c7b | approve comment 28021, rev 826 committed e6948d92; 8 node suites and 9 shell suites rc=0 with Docker disabled (release 4, task 26, Docker cases skipped); 6 probes pass; 7 mutants, 5 killed by candidate tests and 2 by extra probes; three non-blocking follow-ups; rebase round follows row 63 landing per decision 125; no push +2026-10-12T22:59:53Z | filbert | row 63 (#1556) round 2 static review on 2a22fd3f, base cca5f23e | reconstructed 6/6, no drift; implementation unchanged, four new child-path proofs reviewed; verification script ready and syntax checked; no tests or verdict yet, Dewey has the booked test slot after Darkwing; resume on Dewey scratch-clear