From c8a7389615d42854347239c7f8bc36db37b7d266 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 11 Oct 2026 13:22:50 -0500 Subject: [PATCH] vikunja: Q61a #19 install, bus on #19, SetSpark #35 and Launchpad #36; shares read (sage) install-19.mjs renames 57 done, 159 todo, 161 in-progress, 162 blocked, adds 248 in-review and moves the default to 159; no task moved. Writes go to /api/v1 (v2.7.0 405s bucket writes under /api/v2). create-33.mjs adds #35 and #36 under #33. Records hold no secret; shares-read titles removed. Co-Authored-By: Claude Opus 5.5 --- BUILD-LOG.md | 12 + .../vikunja-setup/2026-10-11_before-19.json | 373 ++++++++++++++++ .../vikunja-setup/2026-10-11_create-33.json | 29 ++ .../2026-10-11_install-19-attempt1.json | 17 + .../vikunja-setup/2026-10-11_install-19.json | 137 ++++++ .../vikunja-setup/2026-10-11_shares-read.json | 420 ++++++++++++++++++ agents/sage/work/vikunja-setup/before-19.mjs | 55 +++ agents/sage/work/vikunja-setup/create-33.mjs | 58 +++ agents/sage/work/vikunja-setup/install-19.mjs | 96 ++++ .../sage/work/vikunja-setup/shares-read.mjs | 49 ++ 10 files changed, 1246 insertions(+) create mode 100644 agents/sage/work/vikunja-setup/2026-10-11_before-19.json create mode 100644 agents/sage/work/vikunja-setup/2026-10-11_create-33.json create mode 100644 agents/sage/work/vikunja-setup/2026-10-11_install-19-attempt1.json create mode 100644 agents/sage/work/vikunja-setup/2026-10-11_install-19.json create mode 100644 agents/sage/work/vikunja-setup/2026-10-11_shares-read.json create mode 100644 agents/sage/work/vikunja-setup/before-19.mjs create mode 100644 agents/sage/work/vikunja-setup/create-33.mjs create mode 100644 agents/sage/work/vikunja-setup/install-19.mjs create mode 100644 agents/sage/work/vikunja-setup/shares-read.mjs diff --git a/BUILD-LOG.md b/BUILD-LOG.md index 9a6961408..a00332ad8 100644 --- a/BUILD-LOG.md +++ b/BUILD-LOG.md @@ -4182,3 +4182,15 @@ Gate (Sage, scratch worktree of 64b0f9f1 plus the candidate, 12:32Z, Docker up): Not done yet: the brief's recorded run, in which the PM launches a coder and `mosaic agents` shows both. Filbert does it on a scratch data root against fe5c68fa. Row 41 stays in-review and #1523 stays open until then (comment 27223). Rows 57 to 61, 42 and 49 wait on row 41 done. Decision 77 restart: mosaic-bus@mosaic-stack restarted at 2026-10-11T17:25:49Z on 558699bc (fe5c68fa plus the row 55 queue commit), PID 63113, "bus host up", no host refusal. The tracker didn't start: `tasks mosaic-stack startup tracker-project`. That isn't from S6. The previous process had logged `tick tracker-not-found` every 30 s since 17:19:41Z, after logging no tracker errors from 16:03:37Z. Probes at 17:26:40Z: the sync bot gets 404 and the pm bot 401 on project 32's tasks. Something on tasks.woltje.com changed at about 17:19Z. Reported to Mos with the Q53 reply. The bus needs a restart after the fix, because a `tracker-project` refusal holds until restart. + +### 2026-10-11 — Sage, Q61a: shares read, keys re-minted, bus on #19, SetSpark and Launchpad (decision 86) + +Before: the bus refused with `tracker-project` (project 32 deleted), the bots saw no project but #32, and #19 "Mosaic Stack" had a kanban with Jason's own buckets. After, in order, each with its record in `agents/sage/work/vikunja-setup/`: + +- Shares (ops-01, team_projects 1-6, 18:12:52Z). `shares-read.mjs`: pm and sync each list 29 projects, with #3, #4 and #33 and their descendants (#19's children 18, 20, 21; #34 under #33). cto, coder and reviewer get 401 on `GET /projects`, by scope. The v2 list returns no permission level, so the share levels were not read back; no write was tried as a test. Record `2026-10-11_shares-read.json`. +- Keys (`remint.mjs`, 80604260). pm key 8 adds projects create and the bucket and view writes; sync key 9 adds projects read_all. Keys 7 and 6 revoked, each answering 401. No key holds a task, project, view or bucket delete or a share route. +- #19 board (`install-19.mjs`, pm writes, sync reads). Before-state `2026-10-11_before-19.json`: view 76, buckets 55 To-Do (0 tasks), 56 Doing (0), 57 Done (12, done), 158 Backlog (10), 159 To Do (3), 160 Pending (1), 161 In Progress (3), 162 Blocked (1). Renamed 57 to done, 159 to todo, 161 to in-progress, 162 to blocked; added 248 in-review between them; default 55 to 159, done stays 57. No task moved and none was deleted. 55, 56, 158 and 160 stay as extra buckets. Mos named To-Do and Doing for the rename; they're empty, and Jason's open work sits in To Do and In Progress, so those took the names. A first attempt (`2026-10-11_install-19-attempt1.json`) got 405 on its first write and changed nothing: v2.7.0 serves bucket and view writes under `/api/v1` only (`GET /api/v2/routes`). Record `2026-10-11_install-19.json`, problems []. +- Bus. `.mosaic/project.json` tracker.project 32 to 19 (5628885a); restart at 18:19:28Z, PID 1242258. The first reconcile recorded 30 `task.changed.external` for vikunja:19/* and `task.missing` (not-found) for vikunja:32/144. No tasks line in the journal through 18:22:15Z, about five ticks, and no snapshot after the reconcile. Startup errors do reach the journal (17:25:50Z `startup tracker-project`), so the silence is a pass. +- Projects (`create-33.mjs`). #35 SetSpark and #36 Launchpad under #33, no tasks, read back by the sync key through the #33 share. Vikunja gave each its default views and buckets; they are watch-only children and nobody touches them. + +Rollback: renames back to the before-state titles and default back to 55 (pm can do both); bucket 248 and projects 35 and 36 need Jason or ops-01 to delete, or the pm archives the projects; project.json revert plus restart; shares are ops-01's DELETE of ids 1-6; keys can't be restored, only re-minted. Rows 62 and 63 carry the multi-project adapter work. diff --git a/agents/sage/work/vikunja-setup/2026-10-11_before-19.json b/agents/sage/work/vikunja-setup/2026-10-11_before-19.json new file mode 100644 index 000000000..2e22989b1 --- /dev/null +++ b/agents/sage/work/vikunja-setup/2026-10-11_before-19.json @@ -0,0 +1,373 @@ +{ + "at": "2026-10-11T18:15:27.432Z", + "project": { + "id": 19, + "title": "Mosaic Stack", + "parent": 33, + "archived": false, + "done_bucket_id": null, + "default_bucket_id": null + }, + "views": [ + { + "id": 73, + "title": "List", + "kind": "list", + "position": 100, + "mode": "none", + "done_bucket_id": 0, + "default_bucket_id": 0, + "filter": { + "s": "", + "sort_by": null, + "order_by": null, + "filter": "done = false", + "filter_include_nulls": false + } + }, + { + "id": 74, + "title": "Gantt", + "kind": "gantt", + "position": 200, + "mode": "none", + "done_bucket_id": 0, + "default_bucket_id": 0, + "filter": null + }, + { + "id": 75, + "title": "Table", + "kind": "table", + "position": 300, + "mode": "none", + "done_bucket_id": 0, + "default_bucket_id": 0, + "filter": null + }, + { + "id": 76, + "title": "Kanban", + "kind": "kanban", + "position": 400, + "mode": "manual", + "done_bucket_id": 57, + "default_bucket_id": 55, + "filter": null, + "buckets": [ + { + "id": 55, + "title": "To-Do", + "position": 100, + "limit": 0, + "count": 0, + "tasks": [] + }, + { + "id": 56, + "title": "Doing", + "position": 200, + "limit": 0, + "count": 0, + "tasks": [] + }, + { + "id": 57, + "title": "Done", + "position": 300, + "limit": 0, + "count": 0, + "tasks": [ + 97, + 96, + 95, + 93, + 92, + 91, + 89, + 88, + 84, + 83, + 81, + 80 + ] + }, + { + "id": 158, + "title": "Backlog", + "position": 10354688, + "limit": 0, + "count": 0, + "tasks": [ + 87, + 85, + 82, + 77, + 76, + 75, + 74, + 73, + 71, + 69 + ] + }, + { + "id": 159, + "title": "To Do", + "position": 10420224, + "limit": 0, + "count": 0, + "tasks": [ + 90, + 86, + 72 + ] + }, + { + "id": 160, + "title": "Pending", + "position": 10485760, + "limit": 0, + "count": 0, + "tasks": [ + 2 + ] + }, + { + "id": 161, + "title": "In Progress", + "position": 10551296, + "limit": 0, + "count": 0, + "tasks": [ + 79, + 78, + 70 + ] + }, + { + "id": 162, + "title": "Blocked", + "position": 10616832, + "limit": 0, + "count": 0, + "tasks": [ + 94 + ] + } + ] + } + ], + "tasks": [ + { + "id": 2, + "title": "Revisit Agent Harness Strategy Planning Session", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 69, + "title": "Fix Mosaic Stack Chat UI — CSS Variables + LLM Backend", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 70, + "title": "v0.0.15 Full Dashboard Styling + Platform Features Orchestration", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 71, + "title": "Implement Agent Harness Wrapper (Claude Code + OpenCode)", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 72, + "title": "Implement Non-AI Coordinator Phase 0 (Foundation)", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 73, + "title": "Update Woodpecker Secrets for Gitea Packages (Mosaic Stack)", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 74, + "title": "Migrate Harbor to Gitea Packages (Homelab)", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 75, + "title": "Mosaic Stack Go-Live: Deploy and pass smoke checks", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 76, + "title": "Mosaic Stack Go-Live: One full agent cycle visible", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 77, + "title": "Mosaic Stack Go-Live: Task and status ingestion working", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 78, + "title": "Mosaic Stack Go-Live: Core dashboard operational", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 79, + "title": "Mosaic Stack: Go-Live MVP to Unblock Projects", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 80, + "title": "Mosaic Stack: Ongoing authentication flow issue", + "done": true, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 81, + "title": "Agent Skills Fleet — mosaic/agent-skills repo (94 skills)", + "done": true, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 82, + "title": "Mosaic Stack: Auto-generate Synapse homeserver.yaml in deployment script", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 83, + "title": "Research OpenClaw and Claude Cowork for Mosaic Stack", + "done": true, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 84, + "title": "TTS/STT Platform Research for Mosaic Stack", + "done": true, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 85, + "title": "Mosaic Stack: UI Reference — OpenClaw Mission Control Patterns", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 86, + "title": "Mosaic Stack: Node 20 → 24 LTS Migration (EOL April 2026)", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 87, + "title": "Mosaic Stack: Migrate to Split Woodpecker Pipelines", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 88, + "title": "Mosaic Stack: API & Coordinator Container Issues", + "done": true, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 89, + "title": "Mosaic Stack: Woodpecker Pipeline link-packages Fix", + "done": true, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 90, + "title": "Evaluate OpenCode as multi-provider orchestrator harness", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 91, + "title": "M9-CredentialSecurity (0.0.9) - Credential Security Hardening", + "done": true, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 92, + "title": "Mosaic Stack: M4-LLM Branch Merge (Neglected Branch)", + "done": true, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 93, + "title": "CRITICAL: Mosaic Stack Agent Orchestration Platform Decision", + "done": true, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 94, + "title": "Mosaic Stack PoC - Homelab Deployment", + "done": false, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 95, + "title": "Test quality-rails on uConnect, Syncagent, Inventory Stickers", + "done": true, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 96, + "title": "Create quality-rails repository for portable enforcement", + "done": true, + "bucket_id": 0, + "project_id": 19 + }, + { + "id": 97, + "title": "OpenClaw (formerly MoltBot/ClawdBot) - MAJOR Issues Investigation", + "done": true, + "bucket_id": 0, + "project_id": 19 + } + ] +} diff --git a/agents/sage/work/vikunja-setup/2026-10-11_create-33.json b/agents/sage/work/vikunja-setup/2026-10-11_create-33.json new file mode 100644 index 000000000..7ce84bab9 --- /dev/null +++ b/agents/sage/work/vikunja-setup/2026-10-11_create-33.json @@ -0,0 +1,29 @@ +{ + "at": "2026-10-11T18:21:56.761Z", + "parent": 33, + "created": [ + { + "title": "SetSpark", + "id": 35, + "parent": 33, + "inverse": "archive: POST /api/v1/projects/35 is_archived true (pm); delete is Jason's or ops-01's", + "readBack": 200, + "title_back": "SetSpark", + "parent_back": 33, + "archived": false, + "tasks": 0 + }, + { + "title": "Launchpad", + "id": 36, + "parent": 33, + "inverse": "archive: POST /api/v1/projects/36 is_archived true (pm); delete is Jason's or ops-01's", + "readBack": 200, + "title_back": "Launchpad", + "parent_back": 33, + "archived": false, + "tasks": 0 + } + ], + "problems": [] +} diff --git a/agents/sage/work/vikunja-setup/2026-10-11_install-19-attempt1.json b/agents/sage/work/vikunja-setup/2026-10-11_install-19-attempt1.json new file mode 100644 index 000000000..426c7ac55 --- /dev/null +++ b/agents/sage/work/vikunja-setup/2026-10-11_install-19-attempt1.json @@ -0,0 +1,17 @@ +{ + "at": "2026-10-11T18:18:25.463Z", + "project": 19, + "view": 76, + "writes": [ + { + "op": "rename bucket", + "id": 57, + "from": "Done", + "to": "done", + "inverse": "POST /projects/19/views/76/buckets/57 title \"Done\"" + } + ], + "problems": [ + "stopped: rename 57: 405 Method Not Allowed" + ] +} diff --git a/agents/sage/work/vikunja-setup/2026-10-11_install-19.json b/agents/sage/work/vikunja-setup/2026-10-11_install-19.json new file mode 100644 index 000000000..892b4f5c6 --- /dev/null +++ b/agents/sage/work/vikunja-setup/2026-10-11_install-19.json @@ -0,0 +1,137 @@ +{ + "at": "2026-10-11T18:19:01.514Z", + "project": 19, + "view": 76, + "writes": [ + { + "op": "rename bucket", + "id": 57, + "from": "Done", + "to": "done", + "inverse": "POST /api/v1/projects/19/views/76/buckets/57 title \"Done\"", + "result": { + "title": "done", + "position": 300, + "limit": 0 + } + }, + { + "op": "rename bucket", + "id": 159, + "from": "To Do", + "to": "todo", + "inverse": "POST /api/v1/projects/19/views/76/buckets/159 title \"To Do\"", + "result": { + "title": "todo", + "position": 10420224, + "limit": 0 + } + }, + { + "op": "rename bucket", + "id": 161, + "from": "In Progress", + "to": "in-progress", + "inverse": "POST /api/v1/projects/19/views/76/buckets/161 title \"In Progress\"", + "result": { + "title": "in-progress", + "position": 10551296, + "limit": 0 + } + }, + { + "op": "rename bucket", + "id": 162, + "from": "Blocked", + "to": "blocked", + "inverse": "POST /api/v1/projects/19/views/76/buckets/162 title \"Blocked\"", + "result": { + "title": "blocked", + "position": 10616832, + "limit": 0 + } + }, + { + "op": "add bucket", + "title": "in-review", + "position": 10584064, + "id": 248, + "inverse": "Jason deletes bucket 248 (no bot holds bucket delete)" + }, + { + "op": "view default bucket", + "from": 55, + "to": 159, + "inverse": "POST /api/v1/projects/19/views/76 default_bucket_id 55", + "result": { + "default": 159, + "done": 57, + "mode": "manual", + "filter": null + } + } + ], + "problems": [], + "after": { + "done": 57, + "default": 159, + "mode": "manual", + "filter": null, + "buckets": [ + { + "id": 55, + "title": "To-Do", + "position": 100, + "tasks": 0 + }, + { + "id": 56, + "title": "Doing", + "position": 200, + "tasks": 0 + }, + { + "id": 57, + "title": "done", + "position": 300, + "tasks": 12 + }, + { + "id": 158, + "title": "Backlog", + "position": 10354688, + "tasks": 10 + }, + { + "id": 159, + "title": "todo", + "position": 10420224, + "tasks": 3 + }, + { + "id": 160, + "title": "Pending", + "position": 10485760, + "tasks": 1 + }, + { + "id": 161, + "title": "in-progress", + "position": 10551296, + "tasks": 3 + }, + { + "id": 248, + "title": "in-review", + "position": 10584064, + "tasks": 0 + }, + { + "id": 162, + "title": "blocked", + "position": 10616832, + "tasks": 1 + } + ] + } +} diff --git a/agents/sage/work/vikunja-setup/2026-10-11_shares-read.json b/agents/sage/work/vikunja-setup/2026-10-11_shares-read.json new file mode 100644 index 000000000..7e36f7408 --- /dev/null +++ b/agents/sage/work/vikunja-setup/2026-10-11_shares-read.json @@ -0,0 +1,420 @@ +{ + "at": "2026-10-11T18:14:52.608Z", + "seen": { + "pm": { + "status": 200, + "projects": [ + { + "id": 3, + "parent": 0, + "archived": false, + "max": null + }, + { + "id": 4, + "parent": 0, + "archived": false, + "max": null + }, + { + "id": 6, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 7, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 8, + "parent": 34, + "archived": false, + "max": null + }, + { + "id": 11, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 12, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 13, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 14, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 15, + "parent": 4, + "archived": true, + "max": null + }, + { + "id": 17, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 18, + "parent": 19, + "archived": false, + "max": null + }, + { + "id": 20, + "parent": 19, + "archived": false, + "max": null + }, + { + "id": 21, + "parent": 19, + "archived": false, + "max": null + }, + { + "id": 22, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 23, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 24, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 27, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 28, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 29, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 33, + "parent": 0, + "archived": false, + "max": null + }, + { + "id": 5, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 26, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 34, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 9, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 10, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 16, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 19, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 25, + "parent": 33, + "archived": false, + "max": null + } + ] + }, + "cto": { + "status": 401, + "projects": [] + }, + "coder": { + "status": 401, + "projects": [] + }, + "reviewer": { + "status": 401, + "projects": [] + }, + "sync": { + "status": 200, + "projects": [ + { + "id": 3, + "parent": 0, + "archived": false, + "max": null + }, + { + "id": 4, + "parent": 0, + "archived": false, + "max": null + }, + { + "id": 6, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 7, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 8, + "parent": 34, + "archived": false, + "max": null + }, + { + "id": 11, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 12, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 13, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 14, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 15, + "parent": 4, + "archived": true, + "max": null + }, + { + "id": 17, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 18, + "parent": 19, + "archived": false, + "max": null + }, + { + "id": 20, + "parent": 19, + "archived": false, + "max": null + }, + { + "id": 21, + "parent": 19, + "archived": false, + "max": null + }, + { + "id": 22, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 23, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 24, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 27, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 28, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 29, + "parent": 4, + "archived": false, + "max": null + }, + { + "id": 33, + "parent": 0, + "archived": false, + "max": null + }, + { + "id": 5, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 26, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 34, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 9, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 10, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 16, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 19, + "parent": 33, + "archived": false, + "max": null + }, + { + "id": 25, + "parent": 33, + "archived": false, + "max": null + } + ] + } + }, + "problems": [ + "cto sees a different project set than sync", + "coder sees a different project set than sync", + "reviewer sees a different project set than sync" + ], + "descendants": { + "3": [], + "4": [ + 6, + 7, + 11, + 12, + 13, + 14, + 15, + 17, + 22, + 23, + 24, + 27, + 28, + 29 + ], + "33": [ + 8, + 18, + 20, + 21, + 5, + 26, + 34, + 9, + 10, + 16, + 19, + 25 + ] + }, + "childOf33": { + "id": 8, + "project": 200, + "tasks": 200, + "firstPage": 1 + }, + "note": "Project titles removed from the repo copy (Jason's personal projects); ids, parents and archived flags kept." +} diff --git a/agents/sage/work/vikunja-setup/before-19.mjs b/agents/sage/work/vikunja-setup/before-19.mjs new file mode 100644 index 000000000..8decae663 --- /dev/null +++ b/agents/sage/work/vikunja-setup/before-19.mjs @@ -0,0 +1,55 @@ +// Sage, 2026-10-11: read-only, Q61a. With the sync key, saves project #19's state before the agents +// install the five buckets: the project, its views, each kanban view's buckets, the tasks in each +// bucket, and every task's done flag. GETs only; refuses to overwrite OUT; prints no secret. +// Usage: node before-19.mjs https://tasks.woltje.com SYNC_TOKEN_FILE OUT_JSON +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +const [ORIGIN, KEY, OUT] = process.argv.slice(2); +if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only"); +if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing"); +const BASE = ORIGIN + "/api/v2", P = 19; +const key = readFileSync(KEY, "utf8").trim(); +const get = async (path) => { + const r = await fetch(BASE + path, { headers: { Authorization: `Bearer ${key}` }, redirect: "error" }); + const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {} + if (r.status !== 200) throw new Error(`GET ${path}: ${r.status} ${json?.code ?? ""} ${json?.message ?? ""}`); + return json; +}; +const list = (j) => j?.items ?? (Array.isArray(j) ? j : []); +async function all(path) { + const out = []; + for (let page = 1; page < 100; page++) { + const items = list(await get(`${path}${path.includes("?") ? "&" : "?"}per_page=50&page=${page}`)); + out.push(...items); + if (items.length < 50) return out; + } + throw new Error(`${path}: more than 99 pages`); +} +const p = await get(`/projects/${P}`); +const rec = { + at: new Date().toISOString(), + project: { id: p.id, title: p.title, parent: p.parent_project_id, archived: p.is_archived, done_bucket_id: p.done_bucket_id ?? null, default_bucket_id: p.default_bucket_id ?? null }, + views: [], tasks: [], +}; +for (const v of await all(`/projects/${P}/views`)) { + const view = { id: v.id, title: v.title, kind: v.view_kind, position: v.position, mode: v.bucket_configuration_mode, done_bucket_id: v.done_bucket_id, default_bucket_id: v.default_bucket_id, filter: v.filter ?? null }; + if (v.view_kind === "kanban") { + view.buckets = (await all(`/projects/${P}/views/${v.id}/buckets`)).map((b) => ({ id: b.id, title: b.title, position: b.position, limit: b.limit, count: b.count, tasks: [] })); + // The whole board, done tasks included, as the adapter reads it (sync.mjs board()). + const byId = new Map(view.buckets.map((b) => [b.id, b])); + for (let page = 1; page < 100; page++) { + let more = false; + for (const b of list(await get(`/projects/${P}/views/${v.id}/buckets/tasks?per_page=50&page=${page}`))) { + const tasks = b.tasks ?? []; + byId.get(b.id)?.tasks.push(...tasks.map((t) => t.id)); + if (Number.isSafeInteger(b.count) ? b.count > page * 50 : tasks.length >= 50) more = true; + } + if (!more) break; + } + } + rec.views.push(view); +} +rec.tasks = (await all(`/projects/${P}/tasks`)).map((t) => ({ id: t.id, title: t.title, done: t.done, bucket_id: t.bucket_id ?? null, project_id: t.project_id })); +const out = JSON.stringify(rec, null, 1) + "\n"; +if (out.includes(key)) throw new Error("secret in output; not written"); +writeFileSync(OUT, out, { flag: "wx" }); +process.stdout.write(out); diff --git a/agents/sage/work/vikunja-setup/create-33.mjs b/agents/sage/work/vikunja-setup/create-33.mjs new file mode 100644 index 000000000..1e9c0a26c --- /dev/null +++ b/agents/sage/work/vikunja-setup/create-33.mjs @@ -0,0 +1,58 @@ +// Sage, 2026-10-11: Q61a, the first real work. The pm key creates "SetSpark" and "Launchpad" under +// #33 "Business", with no tasks. Refuses if #33 already has a child by either title. The sync key +// reads each new project back. Rollback is an archive with the pm key; deletion is Jason's or +// ops-01's. Prints no secret. +// Usage: node create-33.mjs https://tasks.woltje.com PM_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2); +if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only"); +if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing"); +// v2.7.0 serves reads under /api/v2; project create is PUT /api/v1/projects (GET /api/v2/routes). +const PARENT = 33, TITLES = ["SetSpark", "Launchpad"]; +const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim(); +async function api(method, path, body, auth) { + const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" }); + const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {} + return { status: r.status, json }; +} +const list = (j) => j?.items ?? (Array.isArray(j) ? j : []); +async function projects() { + const out = []; + for (let page = 1; page < 20; page++) { + const r = await api("GET", `/api/v2/projects?per_page=50&page=${page}&is_archived=true`, undefined, sync); + if (r.status !== 200) throw new Error(`list projects: ${r.status}`); + out.push(...list(r.json)); + if (list(r.json).length < 50) return out; + } + throw new Error("more than 19 pages"); +} +const rec = { at: new Date().toISOString(), parent: PARENT, created: [], problems: [] }; +const save = () => { + const out = JSON.stringify(rec, null, 1) + "\n"; + if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written"); + writeFileSync(OUT, out); +}; +const before = await projects(); +if (!before.some((p) => p.id === PARENT)) throw new Error(`#${PARENT} not visible`); +const clash = before.filter((p) => TITLES.includes(p.title)); +if (clash.length) throw new Error(`a project titled ${clash.map((p) => `${p.title} (#${p.id}, parent ${p.parent_project_id})`).join(", ")} exists; refusing`); +writeFileSync(OUT, "", { flag: "wx" }); + +for (const title of TITLES) { + rec.created.push({ title }); + save(); + const r = await api("PUT", "/api/v1/projects", { title, parent_project_id: PARENT }, key); + if (r.status >= 300 || !Number.isSafeInteger(r.json?.id)) { + rec.problems.push(`create ${title}: ${r.status} ${r.json?.code ?? ""} ${r.json?.message ?? ""}`); + break; + } + Object.assign(rec.created.at(-1), { id: r.json.id, parent: r.json.parent_project_id, inverse: `archive: POST /api/v1/projects/${r.json.id} is_archived true (pm); delete is Jason's or ops-01's` }); + const back = await api("GET", `/api/v2/projects/${r.json.id}`, undefined, sync); + const tasks = await api("GET", `/api/v2/projects/${r.json.id}/tasks?per_page=1`, undefined, sync); + Object.assign(rec.created.at(-1), { readBack: back.status, title_back: back.json?.title, parent_back: back.json?.parent_project_id, archived: back.json?.is_archived, tasks: tasks.status === 200 ? list(tasks.json).length : `status ${tasks.status}` }); + if (back.status !== 200 || back.json?.title !== title || back.json?.parent_project_id !== PARENT || back.json?.is_archived) rec.problems.push(`${title} read-back differs`); + if (tasks.status !== 200 || list(tasks.json).length !== 0) rec.problems.push(`${title} task read not empty or failed`); +} +save(); +process.stdout.write(JSON.stringify(rec, null, 1) + "\n"); +if (rec.problems.length) process.exit(2); diff --git a/agents/sage/work/vikunja-setup/install-19.mjs b/agents/sage/work/vikunja-setup/install-19.mjs new file mode 100644 index 000000000..3ff5682d8 --- /dev/null +++ b/agents/sage/work/vikunja-setup/install-19.mjs @@ -0,0 +1,96 @@ +// Sage, 2026-10-11: Q61a. The pm key installs the adapter's five buckets on #19's kanban (view 76) by +// renaming the populated buckets whose meaning is plain, adding in-review, and moving the default to +// todo. Every task stays in its bucket; nothing is deleted. The board must still match the +// before-state file, or the script refuses before any write. Each write is logged with its inverse +// as it happens, so a partial run leaves a rollback list. The sync key does every read, as the +// adapter's install check does; the pm key only writes. Prints no secret. +// Usage: node install-19.mjs https://tasks.woltje.com PM_TOKEN_FILE SYNC_TOKEN_FILE BEFORE_JSON OUT_JSON +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +const [ORIGIN, KEY, SYNC, BEFORE, OUT] = process.argv.slice(2); +if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only"); +if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing"); +// v2.7.0 serves the reads under /api/v2 and the bucket and view writes under /api/v1 (GET /api/v2/routes). +const BASE = ORIGIN + "/api/v2", WRITE = ORIGIN + "/api/v1", P = 19, V = 76; +const RENAME = { 159: ["To Do", "todo"], 161: ["In Progress", "in-progress"], 162: ["Blocked", "blocked"], 57: ["Done", "done"] }; +const ADD = { title: "in-review", after: 161, before: 162 }; +const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim(); +const before = JSON.parse(readFileSync(BEFORE, "utf8")); +async function api(method, path, body, auth = key) { + const r = await fetch((method === "GET" ? BASE : WRITE) + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" }); + const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {} + return { status: r.status, json }; +} +const must = (r, l) => { if (r.status >= 300) throw new Error(`${l}: ${r.status} ${r.json?.code ?? ""} ${r.json?.message ?? ""}`); return r.json; }; +const list = (j) => j?.items ?? (Array.isArray(j) ? j : []); +async function board() { + const view = list(must(await api("GET", `/projects/${P}/views?per_page=50`, undefined, sync), "views")).find((v) => v.id === V); + if (!view) throw new Error("view 76 not listed"); + const buckets = list(must(await api("GET", `/projects/${P}/views/${V}/buckets?per_page=50`, undefined, sync), "buckets")); + const tasks = Object.fromEntries(buckets.map((b) => [b.id, []])); + for (let page = 1; page < 100; page++) { + let more = false; + for (const b of list(must(await api("GET", `/projects/${P}/views/${V}/buckets/tasks?per_page=50&page=${page}`, undefined, sync), "board"))) { + (tasks[b.id] ??= []).push(...(b.tasks ?? []).map((t) => t.id)); + if (Number.isSafeInteger(b.count) ? b.count > page * 50 : (b.tasks ?? []).length >= 50) more = true; + } + if (!more) break; + } + for (const id of Object.keys(tasks)) tasks[id].sort((a, b) => a - b); + return { view, buckets, tasks }; +} +const rec = { at: new Date().toISOString(), project: P, view: V, writes: [], problems: [] }; +const save = () => { + const out = JSON.stringify(rec, null, 1) + "\n"; + if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written"); + writeFileSync(OUT, out); +}; + +// Precondition: the board is the before-state board, bucket for bucket and task for task. +const kb = before.views.find((v) => v.id === V); +const now = await board(); +const was = Object.fromEntries(kb.buckets.map((b) => [b.id, { title: b.title, tasks: [...b.tasks].sort((a, c) => a - c) }])); +const is = Object.fromEntries(now.buckets.map((b) => [b.id, { title: b.title, tasks: now.tasks[b.id] ?? [] }])); +if (JSON.stringify(was) !== JSON.stringify(is)) throw new Error("board differs from the before-state; refusing"); +if (now.view.done_bucket_id !== 57 || now.view.default_bucket_id !== 55 || now.view.bucket_configuration_mode !== "manual") throw new Error("view 76 differs from the before-state; refusing"); +writeFileSync(OUT, "", { flag: "wx" }); + +try { + for (const [id, [from, to]] of Object.entries(RENAME)) { + const b = now.buckets.find((x) => x.id === Number(id)); + if (b.title !== from) throw new Error(`bucket ${id} is ${JSON.stringify(b.title)}, not ${JSON.stringify(from)}`); + rec.writes.push({ op: "rename bucket", id: b.id, from, to, inverse: `POST /api/v1/projects/${P}/views/${V}/buckets/${b.id} title ${JSON.stringify(from)}` }); + save(); + const r = must(await api("POST", `/projects/${P}/views/${V}/buckets/${b.id}`, { ...b, title: to }), `rename ${id}`); + rec.writes.at(-1).result = { title: r.title, position: r.position, limit: r.limit }; + if (r.title !== to || r.position !== b.position || r.limit !== b.limit) rec.problems.push(`bucket ${id} came back ${JSON.stringify(rec.writes.at(-1).result)}`); + } + const pos = (id) => now.buckets.find((x) => x.id === id).position; + const position = (pos(ADD.after) + pos(ADD.before)) / 2; + rec.writes.push({ op: "add bucket", title: ADD.title, position }); + save(); + const added = must(await api("PUT", `/projects/${P}/views/${V}/buckets`, { title: ADD.title, position, project_view_id: V }), "add in-review"); + Object.assign(rec.writes.at(-1), { id: added.id, inverse: `Jason deletes bucket ${added.id} (no bot holds bucket delete)` }); + const view = now.view; + rec.writes.push({ op: "view default bucket", from: view.default_bucket_id, to: 159, inverse: `POST /api/v1/projects/${P}/views/${V} default_bucket_id ${view.default_bucket_id}` }); + save(); + const v = must(await api("POST", `/projects/${P}/views/${V}`, { ...view, default_bucket_id: 159 }), "view default"); + rec.writes.at(-1).result = { default: v.default_bucket_id, done: v.done_bucket_id, mode: v.bucket_configuration_mode, filter: v.filter ?? null }; +} catch (e) { + rec.problems.push(`stopped: ${e.message}`); + save(); + process.stdout.write(JSON.stringify(rec, null, 1) + "\n"); + process.exit(1); +} + +// Read back: the adapter's install rule, and every task still in the bucket it started in. +const after = await board(); +const titles = ["todo", "in-progress", "in-review", "blocked", "done"]; +const ids = Object.fromEntries(titles.map((t) => [t, after.buckets.filter((b) => b.title === t).map((b) => b.id)])); +rec.after = { done: after.view.done_bucket_id, default: after.view.default_bucket_id, mode: after.view.bucket_configuration_mode, filter: after.view.filter ?? null, buckets: after.buckets.map((b) => ({ id: b.id, title: b.title, position: b.position, tasks: (after.tasks[b.id] ?? []).length })) }; +for (const t of titles) if (ids[t].length !== 1) rec.problems.push(`${t} occurs ${ids[t].length} times`); +if (after.view.done_bucket_id !== ids.done[0] || after.view.default_bucket_id !== ids.todo[0]) rec.problems.push("done or default not wired to done and todo"); +if (JSON.stringify(after.view.filter ?? null) !== JSON.stringify(now.view.filter ?? null)) rec.problems.push("view filter changed"); +for (const [id, w] of Object.entries(was)) if (JSON.stringify(after.tasks[id] ?? []) !== JSON.stringify(w.tasks)) rec.problems.push(`bucket ${id} tasks changed`); +save(); +process.stdout.write(JSON.stringify(rec, null, 1) + "\n"); +if (rec.problems.length) process.exit(2); diff --git a/agents/sage/work/vikunja-setup/shares-read.mjs b/agents/sage/work/vikunja-setup/shares-read.mjs new file mode 100644 index 000000000..b0a66aeee --- /dev/null +++ b/agents/sage/work/vikunja-setup/shares-read.mjs @@ -0,0 +1,49 @@ +// Sage, 2026-10-11: read-only, Q61a step 4. After ops-01's team shares on #3, #4 and #33, each bot +// key lists the projects it sees (id, parent, archived, max_permission). The sync key then reads one +// child of #33 and its first task page. GETs only; prints no secret. +// Usage: node shares-read.mjs https://tasks.woltje.com TOKEN_DIR +import { readFileSync } from "node:fs"; +const [ORIGIN, DIR] = process.argv.slice(2); +if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only"); +const BASE = ORIGIN + "/api/v2"; +const ROLES = ["pm", "cto", "coder", "reviewer", "sync"]; +const keys = Object.fromEntries(ROLES.map((r) => [r, readFileSync(`${DIR}/${r}-vikunja.token`, "utf8").trim()])); +const get = async (path, auth) => { + const r = await fetch(BASE + path, { headers: { Authorization: `Bearer ${auth}` }, redirect: "error" }); + const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {} + return { status: r.status, json }; +}; +async function projects(auth) { + const out = []; + for (let page = 1; page < 20; page++) { + const r = await get(`/projects?per_page=50&page=${page}&is_archived=true`, auth); + if (r.status !== 200) return { status: r.status, items: out }; + const items = r.json?.items ?? (Array.isArray(r.json) ? r.json : []); + out.push(...items); + if (items.length < 50) break; + } + return { status: 200, items: out }; +} +const rec = { at: new Date().toISOString(), seen: {}, problems: [] }; +for (const role of ROLES) { + const l = await projects(keys[role]); + rec.seen[role] = { status: l.status, projects: l.items.map((p) => ({ id: p.id, title: p.title, parent: p.parent_project_id, archived: p.is_archived, max: p.max_permission ?? p.max_right ?? null })) }; +} +const sync = rec.seen.sync.projects; +const byId = new Map(sync.map((p) => [p.id, p])); +const under = (p, root) => { for (let x = p, n = 0; x && n < 50; x = byId.get(x.parent), n++) if (x.id === root) return true; return false; }; +for (const root of [3, 4, 33]) if (!byId.has(root)) rec.problems.push(`sync does not see #${root}`); +rec.descendants = Object.fromEntries([3, 4, 33].map((root) => [root, sync.filter((p) => p.id !== root && under(p, root)).map((p) => p.id)])); +for (const role of ROLES.filter((r) => r !== "sync")) { + const ids = rec.seen[role].projects.map((p) => p.id).sort((a, b) => a - b).join(); + if (ids !== sync.map((p) => p.id).sort((a, b) => a - b).join()) rec.problems.push(`${role} sees a different project set than sync`); +} +const child = rec.descendants[33]?.[0]; +if (child) { + const p = await get(`/projects/${child}`, keys.sync); + const t = await get(`/projects/${child}/tasks?per_page=1`, keys.sync); + rec.childOf33 = { id: child, project: p.status, title: p.json?.title, tasks: t.status, firstPage: (t.json?.items ?? (Array.isArray(t.json) ? t.json : [])).length }; +} else rec.problems.push("no child of #33 visible"); +const out = JSON.stringify(rec, null, 1) + "\n"; +if (Object.values(keys).some((s) => out.includes(s))) throw new Error("secret in output; not printed"); +process.stdout.write(out);