With tracker.descendants on, the tracker project is the root of a tree.
Each reconcile reads the sync bot's project list (archived projects and
negative pseudo-project ids left out) and walks it by parent_project_id.
A child that passes the install checks is read through its board, and
one that doesn't is watch-only (bucket 0) and never refuses the
business. A tick reads each project in the tree. A child answering 403
or 404 leaves the tree only if GET /projects/<id> then answers 403 or
404 too, and its open tasks get the one-by-one read. Task refs carry
the project, so a task moved inside the tree shows under its new ref
and the old ref gets its tombstone. tracker.descendants reaches the adapter
through the business var and packages/cli/src/config.mjs (lead decision
87). Off, the adapter reads as v0 did.
Darkwing built it (agents/darkwing/work/queue-62), in three rounds.
Round 3 was approved by Filbert (27317) and Dewey (27338) on manifest
8b81e836, 13 paths. Their non-blocking notes and Darkwing's follow-ups
(two surviving mutants in test 18, the fake's view-404 code) go to a
follow-up row.
Sage's gate on 918a4a19, sequential and teed: node suites tasks 71,
business 60, bus 80, cli 110 and queue 150, 0 failed; webui 39 of 40,
then 40 of 40 on three reruns (row 62 touches no webui file; a timing
test under load 17 with Dewey's gate running beside it); test-auth 15,
test-conductor 17, test-config 24, test-discord 66,
test-extension-package 18, test-foundation 44, test-queue 27,
test-release 14, test-task 98, all 0 failed (Docker on). edc5de71..HEAD
touches none of the 13 paths.
Live read, 2026-10-11T21:28:03Z, tasks.woltje.com v2.7.0, root #19,
descendants on, sync key only, GETs only (the startup scope probes not
sent): reconcile 15 GETs, 42 snapshots; tick 8 GETs, 0 snapshots.
Children 18, 20 and 21 are watch-only. A missing view id on #18 answers
404 (code 3014) while #18 reads 200, as the README says. Record:
agents/sage/work/vikunja-setup/2026-10-11_tick-62.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
4.0 KiB
S3 live run
run.mjs takes one task through every verb against a real Vikunja, with
the real broker and adapter in one process. It's the row 38 acceptance
run against the estate instance, and it was rehearsed against a scratch
container first.
node packages/tasks/live/run.mjs setup.json out/ --base https://tasks.example.org
--base has to repeat baseUrl from the setup file, or the run exits 2
before it reads a token. A setup file copied from another install can't
send the run to the wrong host without the operator typing that host.
Setup file
{
"business": "demo",
"baseUrl": "https://tasks.example.org",
"project": 12,
"sync": {"botId": 41, "file": "/home/op/.config/mosaic-dev/tokens/demo-sync", "expires": "2027-01-01"},
"roles": {
"pm": {"botId": 42, "env": "VK_PM", "expires": "2027-01-01"},
"coder": {"botId": 43, "env": "VK_CODER", "expires": "2027-01-01"},
"reviewer": {"botId": 44, "env": "VK_REVIEWER", "expires": "2027-01-01"}
},
"labels": {"slice-1": 7}
}
Each token is a file or an environment variable, never a value in this
file. A token file follows the broker's rules: an absolute path, mode
0600, owned by the operator, not a symlink. expires is the day the
token was minted to expire, as YYYY-MM-DD. The project, buckets, bots
and labels come from runbook sections 2 and 3
(docs/guides/slice-1-identities.md). The first label in labels is added at create and removed at schedule.
Leave labels empty to skip both.
What it does
The run builds a business with an operator human and the pm as arbiter
for both domains, then starts the broker with the adapter in a fresh
data directory under TMPDIR. The three roles claim their launches and
the adapter runs its startup checks and one reconcile.
Then the steps, in order. Each one either succeeds or refuses with the code the step expects:
| Step | Role | Verb | Expected |
|---|---|---|---|
| create-by-coder | coder | task.create |
field-writer |
| create | pm | task.create |
ok, with the label |
| assign | pm | task.assign to coder |
ok |
| update-by-unassigned | reviewer | task.update.assigned |
not-assigned |
| start | coder | task.update.assigned in-progress, 25%, comment |
ok |
| conflict | pm | task.priority.change with a wrong expect |
task-conflict |
| priority-undecided | pm | task.priority.change, no decision |
decision-required |
| priority | pm | task.priority.change with a resolved decision |
ok |
| priority-reused | pm | the same decision again | decision-consumed |
| schedule | pm | task.schedule due date, label removed |
ok |
| scope | pm | task.scope.change with a resolved decision |
ok |
| review | coder | task.update.assigned in-review, 100% |
ok |
| reassign | pm | task.reassign to reviewer |
ok |
| close | pm | task.close |
ok |
| after-close | reviewer | task.update.assigned |
task-done |
Priority and scope are cross-role for the pm, so each needs a decision.
The pm raises it, and the operator resolves it through the broker's
human CLI binding. A tick after the start step and a tick and reconcile
at the end must each record 0 snapshots, since the only writes were the
run's own. The run then prints the event counts by kind and the snapshot
counts by source and role, read from bus.sqlite.
It exits 0 when every step went as expected, 1 when one didn't or the run stopped, and 2 on a bad command line or setup file. The task stays in the project, done, with "Safe to delete" in its description.
Project trees
The run reads the setup's one project, with descendants off. It never
reads a child. A tree is checked with read-only GETs instead, listed in
agents/darkwing/work/queue-62/probes.md.
The log
out/live-run.txt, mode 0600, holds verbs, refusal codes, counts and
task refs. It holds no token, title, description or comment. Before it
writes, the run checks the log text for every token and for the word
"bearer". A hit writes nothing and exits 3. The data directory is
removed when the run ends.