Files
stack/agents/sage/work/vikunja-setup/shares-read.mjs
T
jason.woltjeandClaude Opus 5.5 c8a7389615 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 <[email protected]>
2026-10-11 13:22:50 -05:00

50 lines
3.0 KiB
JavaScript

// 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);