Files
stack/agents/darkwing/work/queue-63/build.md
T

162 lines
8.6 KiB
Markdown

# Row 63, task verbs across the tree (row B), build (Darkwing)
Issue #1556. Brief: `docs/plans/2026-10-11_multi-project-sync.md`, section
"Multi-project verbs: task verbs across the tree (row B)" (blob `9301de2e`).
Base `08941d8b`. No commit from the base to the packet's commit touches
the six paths.
`build.patch` is `git diff --cached --binary 08941d8b` of the six paths.
Apply it with `git apply --index` to a detached worktree of the base, and
`sha256sum -c candidate-manifest.sha256` then gives 6/6 OK.
| File | What changed |
|---|---|
| `packages/tasks/src/verbs.mjs` | verbs act in any installed project of the tree |
| `packages/tasks/tests/verbs.test.mjs` | six tests across a root, an installed child and a watch-only grandchild |
| `packages/tasks/tests/tree.test.mjs` | one assertion: the watch-only verb refusal code |
| `packages/tasks/README.md` | the new step, `project`, the new refusal |
| `packages/harness/src/tools.mjs` | `task_create` gets an optional integer `project` |
| `packages/harness/tests/tools.test.mjs` | asserts its type, that it isn't required, that only `task_create` has it |
## What the code does
- `installed(project)` is the one gate. A project the tree doesn't hold
refuses `task-project`, as a foreign ref always has. A project in the
tree with no view, a watch-only one, refuses with the new code
`project-not-installed`. With descendants off the tree holds the root
alone, so nothing changes for a business that hasn't opted in.
- `target()` returns `{project, view, id}` in place of a bare id, and
`read()`, `step()` and every `plan.check` take that. A bucket move, a
close and the board read all use the task's own project and view. No
`ctx.project` or `ctx.view` is left in `read()`, `task.update`,
`task.close` or `create()`'s writes; create's default project is the
one remaining use.
- `relation()` accepts a task in any installed project and keeps its
project. A ref's project is only the caller's claim, so `located()`
reads each related task once with the sync token before
`broker.authorize`. Missing gives the tracker's refusal
(`task-not-found`), a task in another project than its ref names gives
`task-moved`. This covers relations on `task.create` and both the add
and the remove lists of `task.schedule`. Without it, a relation to a
task the ref placed in the child but which sits in a watch-only project
would have been written.
- `task.create` takes an optional `project`, Mos's ruling as the brief
states it. Left out, it's the root. It must be a positive safe integer
(`invalid-request` otherwise, so `"1"`, `1.5`, `0` and `null` refuse
before any read) and an installed project of the tree
(`project-not-installed` for watch-only, unknown and outside alike).
## Choices to review
1. **One code for create's unknown project.** The brief asks for "a new
code the README names" for a watch-only or unknown project on create.
I used `project-not-installed` for both and for a project outside the
tree. A ref outside the tree keeps `task-project`, because a ref names
a task and that code already means "not this business's task". A
create's `project` names where to write, and the brief puts unknown
with watch-only.
2. **Relations are read before authority, which the brief doesn't ask
for.** The brief says `relation()` accepts any installed project's
task. Taking the ref's project on trust would let a caller write a
relation to a task in a watch-only or foreign project by naming an
installed one. One sync read per related task closes that. It costs a
read per relation (create caps relations at 50). The README's verb
steps gain a step 5 for it.
3. **`tree.test.mjs` is outside the brief's owned files.** One assertion
there expected `task-project` for a verb on a watch-only child's task,
which was row A's v0 behaviour. Row B changes that code by design, so
the assertion moves to `project-not-installed` and its comment says
where the installed cases are tested. Nothing else in the file
changed.
4. **The create decision still binds no target.** `task.create`
authorizes with `target: null`, as before, so a decision approving a
create doesn't name which project it approved. A decision raised for
the root can authorize a create into an installed child. The tree is
the business's own and installed projects are writable by design, so
I left it. Binding the project would change the decision record's
shape, which is the broker's and not this row's. It's a follow-up if a
reviewer wants it.
5. **`fake.mjs` is untouched.** `moveToProject` already existed from row
A, which was all the moved-task test needed.
## Tests
The brief asks for each verb across two installed projects, a watch-only
refusal and a relation between projects. The six new tests in
`verbs.test.mjs` run on a tree of root, installed child and watch-only
grandchild (the grandchild holds one task the UI made):
1. Every verb in the installed child, on the child's own board: create
into the child with a relation to a root task, create with the root
named explicitly, assign (and a reassign that refuses
`task-assigned`), `task.update` with a state on the child's view,
`task.scope.change` by the coder and `task.priority.change` by the pm
(each with a decision the cto resolves), reassign, schedule with a due
date and a relation removal, a root-to-child relation add, and close
into the child's done bucket.
2. `task.create` refuses the watch-only grandchild, a project outside
the tree and 999 with `project-not-installed`; `null`, `0`, `-1`,
`1.5`, `"1"` and `[id]` with `invalid-request`. No writes, no allowed
creates.
3. With descendants off, create takes only the root.
4. A verb on the watch-only task and a relation to it refuse
`project-not-installed`; `vikunja:999/1` still gives `task-project`.
5. Relations are read before authority: a ref claiming the child for a
root task gives `task-moved`, a missing child task gives
`task-not-found`, for schedule add, schedule remove and create, with
no writes; then a correct relation goes through.
6. A task moved to another project of the tree refuses its old ref with
`task-moved` and takes its new one.
## Gate
Row 63's gate (tasks, harness, every `scripts/test-*.sh`) plus row A's
business and bus, sequential in the candidate worktree, `TMPDIR` in my
scratch, `test-task.sh` with `DOCKER_HOST=unix:///nonexistent`. It ran
2026-10-12 19:39:19Z to 19:42:45Z, after Sage's row 81 gate and Rocko's
row 77 run had ended. Every suite exits 0 (`gate-summary.txt`).
| Suite | Result |
|---|---|
| tasks | 80 pass (74 at the base plus the six) |
| harness | 59 pass |
| business | 60 pass |
| bus | 95 pass |
| test-auth | 15 passed |
| test-conductor | 17 passed |
| test-config | 24 passed |
| test-discord | 66 passed |
| test-extension-package | 18 passed |
| test-foundation | 44 passed |
| test-queue | 27 passed |
| test-release | 14 passed |
| test-task | 26 passed (the live recall pair skips without Docker) |
## Mutants
`mutants.sh` with `agents/darkwing/work/queue-59-review/r1/mut.mjs`. Each
mutant edits one site (exactly one match), runs `verbs.test.mjs` and
`tree.test.mjs` (or the harness `tools.test.mjs`), and restores the file.
18 of 18 killed (`mutants.txt`, run 2026-10-12 19:20:11Z to 19:21:24Z).
| Mutant | What it undoes | Killed by |
|---|---|---|
| watch-only-writable | the watch-only refusal in `installed()` | assertion, 2 tests |
| create-root-only | create posts to the root whatever `project` says | assertion |
| create-watch-only-ok | `tree.has` in place of `tree.get` on create | assertion |
| create-project-any | the integer check on `project` | assertion |
| move-root-view | a state move looks up the root's bucket ids | `tracker-not-found` |
| move-path-root-view | the bucket move path uses the root's view | `tracker-not-found` |
| close-root-done | close moves to the root's done bucket | `tracker-not-found` |
| read-root-board | `read()` reads the root's board | `task-placement`, 2 tests |
| read-no-project | `read()` drops the project check | assertion (test 6) |
| relation-any-project | `relation()` drops `installed()` | assertion, 2 tests |
| handle-no-located | verbs skip the relation read | assertion |
| create-no-located | create skips the relation read | assertion |
| located-no-project | the relation read drops the project check | assertion |
| schedule-add-only | schedule reads only the add list | assertion |
| located-after-authorize | the relation read moves after authorize | assertion |
| tool-project-string | the tool's `project` becomes a string | assertion |
| tool-project-zero | the tool's minimum becomes 0 | assertion |
| tool-project-required | the tool requires `project` | assertion |