Candidate for #1520 against 81339889: packages/tasks and the bus task
verbs. Live run rehearsed on a scratch v2.7.0 container; the estate run
waits for T236's base URL.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
201 lines
9.2 KiB
Markdown
201 lines
9.2 KiB
Markdown
# Row 38, S3: tasks (the Vikunja adapter, broker task verbs, sync)
|
|
|
|
Darkwing, 2026-10-08. Issue #1520, reviewer Filbert. Brief:
|
|
`docs/plans/2026-10-04_slice-1.md`, section "Slice 1 S3". Design:
|
|
addendum B sections 3, 4 and 7, decisions 68 and 70. Nothing in the
|
|
candidate is committed, staged or pushed.
|
|
|
|
Base is 81339889 (refactor HEAD at gate time). The candidate was built on
|
|
bee89d10, and nothing under `packages/` or `scripts/` changed between the
|
|
two. In a fresh worktree at 81339889 the patch applies and the result
|
|
matches the manifest 28/28.
|
|
|
|
The row can reach review but can't close. The brief's gate wants the live
|
|
run against the instance from row SR, and that waits for T236's base URL.
|
|
What's here is the same run rehearsed against a scratch container.
|
|
|
|
## Files
|
|
|
|
`build.patch` (sha256
|
|
`eb225e5305f50811354604ee45a6e0a7489d9a76543615b2caaba102c13d5700`)
|
|
changes 5 files under `packages/bus/src/` and adds 23.
|
|
`build-manifest.sha256` (sha256
|
|
`be8dbd8ca03ca7264666ba171bd7bffbaaa08c6b265d60e7d587f1bc53897daf`) pins
|
|
all 28 after the patch.
|
|
|
|
New, `packages/tasks/`:
|
|
- `src/vikunja.mjs`: the v2 client. Every call goes through the broker's
|
|
`credentials.use()`, so the adapter never holds a token. Maps Vikunja's
|
|
answers to refusal codes.
|
|
- `src/startup.mjs`: the six startup checks.
|
|
- `src/verbs.mjs`: the eight task verbs.
|
|
- `src/sync.mjs`: the tick (board read plus `updated` cursor), the
|
|
reconcile walk, the comment check and missing-task handling.
|
|
- `src/adapter.mjs`: the factory the broker loads, one serial queue per
|
|
business, timers, credential state, `status()`.
|
|
- `src/digest.mjs`, `src/index.mjs`, `src/fake.mjs` (`FakeVikunja`, the
|
|
recorded fake of the v2 routes).
|
|
- `tests/`: 44 tests in 6 files, `world.mjs` (a broker, credentials and
|
|
the fake wired together) and `fixtures/v2-shapes.json` (responses
|
|
recorded from the pinned image).
|
|
- `deploy/vikunja/compose.yaml` and its README: the bundled option,
|
|
REQ-TASK-3. The upstream image by digest, published on 127.0.0.1, no
|
|
secret in the file. `tests/deploy.test.mjs` checks the digest against
|
|
the runbook and the bind address.
|
|
- `live/run.mjs` and its README: the live run.
|
|
- `README.md`, `package.json`.
|
|
|
|
Changed, `packages/bus/` (registering the task verbs, as the brief allows):
|
|
- `broker.mjs`: `TASK_VERBS`; `requestTask`, which checks the caller,
|
|
runs the secret check on args and result, and records a refusal the
|
|
same way `request` does; `recordTask`, which takes self snapshots from a
|
|
role session and poll snapshots and events from the adapter (cap
|
|
`null`), each from a closed list of event kinds; `taskView`. The
|
|
refusal recording moved into `#refused` so both paths share it.
|
|
- `runtime.mjs`: `startBroker({tasks})` takes the adapter factory,
|
|
refuses an adapter without `handle` or a valid `timeout`, and closes it.
|
|
- `server.mjs`: task verbs go to the adapter, async, with the socket idle
|
|
timeout raised to the adapter's 60 s. Other verbs stay synchronous.
|
|
- `process.mjs`: a plain-data `trackers` map in the boot config loads the
|
|
adapter. Decision 70 puts the poller and reconcile in the broker
|
|
runtime, and this is how the host process gets them.
|
|
- `index.mjs`: exports `TASK_VERBS`.
|
|
- `tests/tasks.test.mjs`: 9 tests for the above.
|
|
|
|
No file outside `packages/bus` and `packages/tasks` changes. The bus
|
|
schema (`task_snapshots`, `task_current`, `tasks_open`) was already in S2.
|
|
|
|
## Probes
|
|
|
|
`probes.md` in this directory has
|
|
every probe of addendum B section 7. The results the brief asked for:
|
|
|
|
- Labels (section L), the first open point. With the runbook as written,
|
|
a bot sees every label its owner made, in any project, and can attach
|
|
them. A bot owned by a separate `svc-$BIZ` account sees only labels on
|
|
tasks it can read. Decision 68 took that: svc owns the bots and the
|
|
labels, and the broker keeps the label-id allowlist as a second guard.
|
|
Startup check 6 refuses a business whose pm can't see its configured
|
|
labels.
|
|
- The async bump lag (section U). Comments, assignees and relations bump
|
|
`updated` from an async listener. On an idle server the bump showed
|
|
0 ms after the 201. Under load one read straight after missed it and
|
|
the next, 5 s later, had it. That bounds the lag at 5 s, and the 60 s
|
|
window decision 68 set is twelve times that. Moving between open
|
|
buckets doesn't bump `updated` at all, which is why every tick reads
|
|
the whole board.
|
|
- probe-s7g (section G) checked the reads the code relied on without a
|
|
probe: the `related_tasks` shape, `assignees` null when empty, done
|
|
tasks in the cursor, `due_date: null` stored as year 1, the missing id,
|
|
the id-keyed walk. Each one held.
|
|
|
|
## Decisions 68 and 70
|
|
|
|
- svc-$BIZ owns the bots: a runbook matter (T236), not code. The adapter
|
|
checks what follows from it, the labels at startup.
|
|
- Label-id allowlist: `tracker.labels`; any other id refuses
|
|
`label-not-allowed`.
|
|
- Poller: a full board read every tick, the cursor from the previous
|
|
tick's start minus 60 s, floored to the second, and a digest dedupe
|
|
against `task_current`.
|
|
- The broker hosts the poller and reconcile: `startBroker({tasks})` and
|
|
the `trackers` boot config.
|
|
|
|
## A defect the tests found
|
|
|
|
The first time the comment check looks at a task, it counts comments
|
|
created at or after a bound. For the reconcile that bound is the start
|
|
of the previous reconcile, to the millisecond. A comment read back from
|
|
`GET /tasks/{t}/comments` has `created` in whole seconds (only the POST
|
|
answer carries nanoseconds, see `fixtures/v2-shapes.json`). So a comment
|
|
posted after the previous reconcile, in the same second, read as older
|
|
than the bound and wasn't counted. `foreignComments` in `sync.mjs` now
|
|
floors the bound to the second. The cost is that on a first look a
|
|
comment up to a second older than the bound can be counted. After the
|
|
first look the check goes by comment id, so this doesn't repeat.
|
|
|
|
## Live run, rehearsed
|
|
|
|
`live-rehearsal.txt` (sha256
|
|
`2a79eda46af5bb20b3aa0f5e5de959320b310a299b1c157c2712df9da74c012f`) is
|
|
`live-run.txt` from the rehearsal: the real
|
|
broker, adapter and socket clients against a scratch v2.7.0 container,
|
|
the pinned digest, on 127.0.0.1 with its own data directory. Nothing
|
|
touched the estate instance or tasks.mosaicstack.dev.
|
|
`rehearse.mjs` and `rehearse-setup.json` are the scratch setup that
|
|
produced it. `rehearse.mjs` registers `svc-demo`, builds the project and
|
|
board, makes the label and four bots, mints their tokens into
|
|
environment variables only, and runs `run.mjs`. It isn't in the
|
|
candidate.
|
|
|
|
The first rehearsal failed the priority and scope steps with
|
|
`decision-required`. The pm holds both verbs cross-role, and the run
|
|
didn't raise a decision. The run now has the pm raise one and the
|
|
operator resolve it through the human CLI binding, and it checks the
|
|
refusal without one and the refusal on reuse. The second rehearsal
|
|
went as expected in every step. Its two ticks and the reconcile after
|
|
the run's own writes recorded 0 snapshots, so the dedupe works against
|
|
a real Vikunja. The log was checked for every token and for "bearer"
|
|
before it was written.
|
|
|
|
For the estate run, the operator writes a setup file with the T236
|
|
project, bot ids, label ids and token files (`live/README.md`), then
|
|
runs `run.mjs` with `--base` repeating the URL. That log goes into this
|
|
directory as `live-run.txt` in a later round.
|
|
|
|
## Gate
|
|
|
|
In the worktree at 81339889 with the patch applied, run one after
|
|
another, each to its own file:
|
|
|
|
| Suite | Result |
|
|
|---|---|
|
|
| `node --test 'packages/tasks/tests/*.test.mjs'` | 44/44 |
|
|
| `node --test 'packages/bus/tests/*.test.mjs'` | 67/67 |
|
|
| `test-auth.sh` | 15/15 |
|
|
| `test-conductor.sh` | 17/17 |
|
|
| `test-config.sh` | 24/24 |
|
|
| `test-discord.sh` | 63/64, see below |
|
|
| `test-extension-package.sh` | 18/18 |
|
|
| `test-foundation.sh` | 44/44 |
|
|
| `test-queue.sh` | 27/27 |
|
|
| `test-release.sh` | 14/14 |
|
|
| `test-task.sh` | 98/98 |
|
|
|
|
After the gate I corrected the `expires` format in `live/README.md`'s
|
|
example (the broker takes `YYYY-MM-DD`, not a timestamp). That's the
|
|
only change since, and the tasks suite passed 44/44 again on it. The
|
|
patch and manifest above are the corrected ones.
|
|
|
|
`test-discord.sh` isn't clean, and I can't show it's unrelated by more
|
|
than reasoning and reruns. Two engine timing tests failed: "when pi has
|
|
not started a timed-out turn by the end of the abort grace..." and "a
|
|
timed-out run pi did start outlives the grace...". Nothing in
|
|
`packages/discord` or `scripts/test-discord.sh` imports `packages/bus`
|
|
or `packages/tasks`. The load average was 22 to 28 from other sessions
|
|
on the host. Reruns, full suite:
|
|
|
|
| Run | With the patch | Base 81339889 |
|
|
|---|---|---|
|
|
| 1 | 63/64 | not run |
|
|
| 2 | 64/64 | 64/64 |
|
|
| 3 | 63/64 | 64/64 |
|
|
| 4 | 64/64 | 64/64 |
|
|
| 5 | 64/64 | 64/64 |
|
|
|
|
`engine.test.mjs` alone passed 6/6 in each tree. Three of five with the
|
|
patch against four of four without is a small sample. If you want it
|
|
settled, the next step is the full suite on a quiet host.
|
|
|
|
## Follow-ups, not in this row
|
|
|
|
- S1's business validator accepts a `tracker.baseUrl` with a path. The
|
|
adapter refuses it at boot with `tracker-config`, which is later than
|
|
it should be.
|
|
- `task.close` records the verdict citation and doesn't check it
|
|
against the queue.
|
|
- A coder's cross-role grant of `task.reassign` can never be used,
|
|
because the field table lets only the pm write the assignee. Either
|
|
the role file drops it or the field table changes.
|
|
- The comment-count bound above.
|