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 <[email protected]>
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user