Files
stack/docs/plans/2026-10-11_multi-project-sync.md
T

152 lines
6.4 KiB
Markdown

# Multi-project sync: rows A and B (2026-10-11)
Status: written by Sage, lead. It follows `docs/plans/BRIEF-TEMPLATE.md`.
Jason answered Q61a, relayed by Mos: agents see and work his nested
projects ("just share them. Get it done."). Mos ruled the shape: row A
now, row B after row A is done, and task.create takes an optional
project. Lead decision 86 records it.
Three constraints hold for both rows. A change that breaks one of them
is out of scope, whatever else it fixes.
- A task's ledger ref stays `vikunja:<project>/<task>`. The ledger
checks that form on `task_snapshots`, `decisions` and the `events`
subject (`packages/bus/schema.sql`), and `task_snapshots` is
append-only. Neither row changes `packages/bus/`. Never key a task by
its id alone.
- `tracker.descendants` defaults to false. With it false, the adapter
behaves exactly as v0 does, and every v0 test passes unchanged.
- The adapter never writes to a board. A child project is installed
only if its one manual kanban view already has the five buckets
(`todo`, `in-progress`, `in-review`, `blocked`, `done`), done and
default wired, as the root's install check requires. Any other child
is watch-only. No bucket, view or project write ships in either row.
## Multi-project sync: the adapter reads a project tree (row A)
### Problem
The adapter watches one project. `settings()` takes one
`tracker.project` (`packages/tasks/src/adapter.mjs:21`), startup checks
that project alone (`startup.mjs:37`), and sync reads its board and task
list only (`sync.mjs:21`, `sync.mjs:45`). A task in any other project is
a shape error in the walk (`sync.mjs:59`), and a move to another project
is a tombstone (`sync.mjs:123`). Jason's projects nest. The bus binds
to #19 "Mosaic Stack", and he wants agents to follow work into its
child projects.
### Owner and reviewer
- Owner: Darkwing.
- Reviewers: Dewey and Filbert.
### Files owned
- `packages/business/src/vars.mjs` and `packages/business/tests/vars.test.mjs`
- `packages/tasks/src/adapter.mjs`, `startup.mjs`, `sync.mjs`,
`fake.mjs`, `vikunja.mjs`, `index.mjs`
- `packages/tasks/tests/` (all files and fixtures)
- `packages/tasks/README.md`, `packages/tasks/live/README.md`
- `agents/darkwing/work/queue-<row>/`
`verbs.mjs` is read-only in this row. Verbs keep refusing a task outside
the root with `task-project`, as they do now.
### What ships
- `tracker.descendants`, a project-layer boolean var, default false.
`tracker.project` stays the root.
- With descendants on, the tree is the root plus every descendant the
sync bot can see through `GET /projects` (key 9 has `projects
read_all`), followed by `parent_project_id`. Discovery runs at startup
and at each reconcile, not each tick. Archived projects are not in the
tree. Record your rule for a project that leaves the tree (archived,
moved out, unshared or deleted) in the README and test it. Its tasks
become `task.missing` with a reason. The ledger is never rewritten.
- Install state per project. The root's install check refuses the
business, exactly as today. A child that passes the same check is
installed. A child that fails it is watch-only and never refuses the
business. Sync records title, done, assignees and comments for a
watch-only child, and no bucket state.
- Sync covers each project in the tree. A task moved between two
projects in the tree gets the `moved` tombstone naming the new project
and a snapshot under its new ref. A move out of the tree stays the
tombstone it is today.
- Poll cost. Probe whether v2.7.0 serves a cross-project task read to the
sync key's scopes. If it does, use one read per tick for the tree. If
not, read per project and state the per-tick call count in the README.
The probe results go in `agents/darkwing/work/queue-<row>/probes.md`.
- `fake.mjs` models `parent_project_id`, the project list route,
archived projects and access inherited from an ancestor's share.
- Tests: descendants off matches v0; a three-level tree; a watch-only
child keeps the business ready; a root without buckets refuses; moves
root to child, child to child and child to outside; a new child appears
at reconcile; a child leaves the tree.
- Gate suites: `node --test 'packages/tasks/tests/*.test.mjs'`,
`node --test 'packages/business/tests/*.test.mjs'`,
`node --test 'packages/bus/tests/*.test.mjs'`, and every
`scripts/test-*.sh`.
### Out of scope
Verbs on child projects (row B). Any change to `packages/bus/`. Any write
to a Vikunja board or project. An archive guard: the adapter has no
project verb, so the pm's `projects.update` can't reach a project through
the bus. A row that adds a project verb adds the guard.
### Gate
Dewey and Filbert approve on the issue, naming the candidate manifest.
The suites above pass on Sage's gate rerun. Sage then runs one live read
against tasks.woltje.com with descendants on, root #19, read-only, and
attaches the tick log.
## Multi-project verbs: task verbs across the tree (row B)
### Problem
After row A, the bus sees child projects but can't act in them.
`target()` and `relation()` refuse a ref from another project
(`verbs.mjs:74`, `verbs.mjs:79`), `read()` refuses a task that isn't in
the root (`verbs.mjs:88`), and `task.create` always posts to the root
(`verbs.mjs:355`) with no project argument.
### Owner and reviewer
- Owner: Darkwing, after row A is done.
- Reviewers: Dewey and Filbert.
### Files owned
- `packages/tasks/src/verbs.mjs`, `fake.mjs`
- `packages/tasks/tests/verbs.test.mjs` and fixtures
- `packages/tasks/README.md`
- `packages/harness/src/tools.mjs` (the `task_create` tool schema gets
the optional project) and its test
- `agents/darkwing/work/queue-<row>/`
### What ships
- Mos's ruling: `task.create` takes an optional `project`. It must be an
installed project in the tree; a watch-only or unknown project refuses
with a new code the README names. Without it, the task goes to the
root, as today.
- `target()`, `relation()` and `read()` accept a task in any installed
project of the tree. A bucket move uses that project's own view and
buckets, not the root's.
- A verb on a task in a watch-only project refuses with the same new
code.
- Tests for each verb across two installed projects, a watch-only refusal
and a relation between projects.
- Gate suites as row A.
### Out of scope
Project verbs (create, rename, archive, move). They need their own
brief, an archive guard and a ruling.
### Gate
Dewey and Filbert approve on the issue, naming the candidate manifest,
and the suites pass on Sage's gate rerun.