Files
stack/agents/darkwing/work/slice1-s3/build-r2.md
T
jason.woltjeandClaude Opus 5.5 b144a03d47 docs(slice1): darkwing row 38 S3 round 2 packet
B1 truncates due dates to the second; B2 records what landed when the
final read fails; tests for V9, V21 and S5. Candidate manifest
e10e30e3, 28 files over c9ef7ee4. Mutants 46/63. Live provider cases
in test-release and test-task failed on a zai usage limit (429).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 19:27:17 -05:00

8.0 KiB

Row 38, S3: round 2

Darkwing, 2026-10-09. Issue #1520, reviewer Filbert. Round 1 review: agents/filbert/work/slice1-s3-review/review-r1.md (comment 26850, rev 183). Scope from Sage (rev 184): B1, B2, and tests for V9, V21 and S5. Nothing in the candidate is committed, staged or pushed.

Base is c9ef7ee4. Nothing under packages/, scripts/ or tools/ changed between bee89d10, where the candidate was built, and c9ef7ee4. In a fresh worktree at c9ef7ee4 the patch applies and the result matches the manifest 28/28.

Files

  • build-r2.patch (sha256 71ce87e631b04d35591e2df966aae565d5f665ac2a9d5d252322ce1b0d5be69c): the whole candidate against c9ef7ee4, the same 28 files as round 1.
  • build-r2-manifest.sha256 (sha256 e10e30e3d68c12177afba438ac02a06c208ea0370c7198af1f9d9cf163d637f4) pins all 28.
  • r1-to-r2.diff: what changed since round 1. Five files under packages/tasks: src/verbs.mjs, src/fake.mjs, README.md, tests/verbs.test.mjs, tests/sync.test.mjs. Nothing in packages/bus changed.
  • r2-filbert-probes.txt, r2-mutants.txt, r2-gate-summary.txt: evidence, below.

B1: due dates with milliseconds

Sage's ruling: truncate in due(), don't refuse. due() now keeps the first 19 characters and appends .000Z. Every use of the value comes after that: the PATCH body, the expected fields for the self snapshot, the digest handed back, and the check an uncertain PATCH's re-read runs. task.create goes through the same due().

FakeVikunja now stores a due date to the second, the way the pinned image does in Filbert's r1-vk-due-probe.txt. A write's answer echoes the value sent, in Go's form, as the image does. The probe can't tell whether Vikunja floors or rounds. The adapter only sends whole seconds, so that doesn't matter to it.

The README says due dates have one-second precision, with an example.

Test: "a due date with milliseconds is written to the second" covers a create with .789Z and a schedule with .456Z. Then:

  • the fake holds whole seconds;
  • the self snapshot holds .000Z, and its digest is the one returned;
  • a tick records nothing;
  • the same schedule again sends no PATCH;
  • the returned digest works as expect;
  • a PATCH whose answer is lost settles on the re-read.

Filbert's probe D1 against this tree shows no external event after the tick and no second PATCH. expect set to the returned digest gets past the conflict check and stops at decision-not-found, the next check, because the probe passes a made-up decision.

B2: a write lands and the final read fails

handle() counts the writes that land. The final read's refusal is no longer passed on:

  • Every write landed: the verb succeeds and records its event and a self snapshot of work.expect(before.fields). updated is the compare-read's, the newest one read. Filbert offered this or a null updated. Keeping the compare-read's value means recordTask didn't have to change.
  • Some landed and a later write failed: write-uncertain, and nothing is recorded. The poll records what the tracker holds.
  • None landed: the failed write's own code, as in round 1.

Filbert's fix allowed refusing write-uncertain or returning when every write landed. I return. The outcome isn't uncertain: each write was confirmed, and the record is what a successful read would have written, apart from updated. A refusal would send the caller back to a verb that then refuses task-assigned, which is the W1 problem again.

Probe W1 against this tree: the caller sees ok, one task.assigned event, one new self snapshot, no external event on the poll, and a retry refuses task-assigned.

One addition beyond Filbert's list. create() had the same shape: the create, labels and relations land, the final read fails, and the caller got tracker-unavailable. That reads as "nothing happened" and invites a second create. Now, when every write landed and the final read failed, the snapshot is built from the create's answer plus the labels written, in the default bucket. task.created is recorded and the verb succeeds. If a label or relation write failed, the verb refuses with that write's code, as in round 1, and task.created is still recorded.

Tests:

  • "every write landed and the final read failed: the verb succeeds and records what it wrote" (assign, Filbert's W1 as a test).
  • "some writes landed and the final read failed: write-uncertain, and nothing is recorded" (schedule: the PATCH lands, the label write gets 403, the final read fails).
  • "a create whose final read fails succeeds and records task.created". A tick afterwards records nothing, so the fallback snapshot matches what the poll reads.

T1, T2, T3

  • T1 (V9): "an edit between the last write and the final read shows as external on the next poll". A UI title edit lands between the assignee write and the final read. The self snapshot keeps the old title, and the tick records changed: ["title"].
  • T2 (V21): "task.created is recorded when a later label write fails".
  • T3 (S5): "the first look at a task counts only comments inside the window". The fake's clock is moved back two hours for the create and one comment, then forward for a second comment. The tick counts 1.

Do the tests catch the round 1 code?

With round 1's src/verbs.mjs and src/fake.mjs under the round 2 tests, four tests fail: the due date test, both final-read tests for handle(), and the create final-read test. T1, T2 and T3 pass there, since they test behaviour round 1 already had. The mutants show they hold it.

Mutants

Filbert's mutants.sh, unchanged, all 63, on a copy of this tree: 46 of 63 killed. V9, V21, S5 and G1 are killed. Round 1 was 42/63. The 17 survivors are round 1's survivors less those four: V12, V19, V20, V23, V24, S3, S6, S7, S9, S10, U5, U6, U7, K1, K3, G2 and B8. Filbert called each of them a test gap or near-equivalent, and none was in this round's scope.

Gate

In the worktree at c9ef7ee4 with the patch applied, run one after another, each to its own file:

Suite Result
node --test 'packages/tasks/tests/*.test.mjs' 51/51
node --test 'packages/bus/tests/*.test.mjs' 67/67
node --test 'packages/business/tests/*.test.mjs' 60/60
node --test 'packages/discord/tests/*.test.mjs' 173/173
test-auth.sh 15/15
test-conductor.sh 17/17
test-config.sh 24/24
test-discord.sh 64/64
test-extension-package.sh 18/18
test-foundation.sh 44/44
test-queue.sh 27/27, the canonical-root check skipped in a scratch tree
test-release.sh 11 pass, 3 fail
test-task.sh 90 pass, 8 fail

The 11 failures are every case that runs a real model turn: the release health check and the live, fork and recall cases in test-task. The zai account had hit its 5-hour usage limit. A sandboxed release.sh activate in the same tree two minutes later left a run whose stderr ends in a 429, "Usage limit reached for 5 hour". The mock-adapter cases in test-task all pass.

I tried a clean base at c9ef7ee4 as a control and it didn't work as one. Docker couldn't create its compose network ("all predefined address pools have been fully subnetted"), so every container case failed there, mock ones included: test-task 67 pass, 31 fail, twice. The candidate tree's network already existed. I didn't remove other networks to make room. r2-gate-summary.txt has the case names and the error lines.

So the live cases are unproven this round, not failed by the patch. Neither suite loads packages/tasks or packages/bus, nothing under scripts/ changed since round 1, and round 1 had 14/14 and 98/98. I'll rerun both suites in the candidate tree after the limit resets and add the result.

Not in this round

Filbert's notes 1 to 14 stay as they were, except note 9 (the fake kept millisecond due dates), which B1 closes. Note 7, secretCheck on the result after a write, has B2's shape. It fails closed, and the fix is a change to the broker's result check, which is outside this round's scope. I'd take it with the follow-ups listed in round 1.