Files
jason.woltje e24a13ad87 sage: Vikunja scripts and read-back records 2026-10-13 to 10-20 (decision 110, mos-0882)
102 files under agents/sage/work/vikunja-setup/: the comment, close and file scripts Sage ran on P19 and their
output records, including P19 386 comment 350 for the mos-0928 Part A window (rows 177 and 180-185). Each script
refuses to write a record that holds a key; a scan for both key values found none.
2026-10-20 03:32:46 -05:00

62 lines
4.5 KiB
JavaScript

// Sage, 2026-10-20: merged B+C window W5 to W9 passed (plan draft 9 line 135). Comments on P19 382
// with the window result, then marks it done. Sage's own key (bot user 13). The update sends the task as the v1
// GET returned it with only done changed; the sync key reads title, description and done back. Rollback is
// marking it undone. Prints no secret.
// Usage: node close-382.mjs https://tasks.woltje.com SAGE_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");
const TASK = 382;
const SIG = "Created by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const p = (...xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(SIG)}</p>`;
const TEXT = [
"Fixed on the live stack by the merged B+C window (2026-10-20). Row 169 built the two-phase PM start (#1666). W5 activated 89af38a83 with --pm-after-harness (20261020T013708Z-89af38a83afa), and the bus came up without its PM. W6: the tester set plugins.harness to [claude-code]. W7 pm-on (20261020T014103Z-89af38a83afa) passed its gate and launched chief rc78c9700c8e9.",
"W5 to W9 passed, so this closes as the window plan says (draft 9, line 135). The full summary is P19 386 comment 324, and the STATE report is agents/darkwing/work/window-merged-bc/STATE.md (sha256 7be42b6bcab5657d).",
];
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 : []);
const rec = { at: new Date().toISOString(), task: TASK, title: null, 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 task = await api("GET", `/api/v2/tasks/${TASK}`, undefined, sync);
if (task.status !== 200 || task.json?.project_id !== 19 || !/c216f47db/.test(task.json?.title ?? "")) throw new Error(`task ${TASK}: ${task.status}, project ${task.json?.project_id}, title doesn't name c216f47db`);
if (task.json.done) throw new Error(`task ${TASK} is already done; refusing`);
const before = { title: task.json.title, description: task.json.description };
rec.title = task.json.title;
writeFileSync(OUT, "", { flag: "wx" });
const c = await api("PUT", `/api/v1/tasks/${TASK}/comments`, { comment: p(...TEXT) }, key);
rec.comment = { status: c.status, id: c.json?.id ?? null };
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) rec.problems.push(`comment: ${c.status} ${c.json?.message ?? ""}`);
else {
const back = await api("GET", `/api/v2/tasks/${TASK}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(rec.comment, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment read-back: ${back.status}`);
}
save();
if (!rec.problems.length) {
const full = await api("GET", `/api/v1/tasks/${TASK}`, undefined, key);
if (full.status !== 200 || full.json?.id !== TASK) rec.problems.push(`v1 get: ${full.status}`);
else {
const u = await api("POST", `/api/v1/tasks/${TASK}`, { ...full.json, done: true }, key);
rec.update = { status: u.status, inverse: `mark undone: POST /api/v1/tasks/${TASK} done false (sage key)` };
const back = await api("GET", `/api/v2/tasks/${TASK}`, undefined, sync);
Object.assign(rec.update, { readBack: back.status, done: back.json?.done, titleSame: back.json?.title === before.title, descriptionSame: back.json?.description === before.description });
if (u.status >= 300 || back.status !== 200 || back.json?.done !== true || !rec.update.titleSame || !rec.update.descriptionSame) rec.problems.push("update or read-back differs");
}
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);