Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
63 lines
4.7 KiB
JavaScript
63 lines
4.7 KiB
JavaScript
// Sage, 2026-10-23: row 320 (#1816, Gladstone, F1-F6) landed 094124b4e on refactor. P19 614 is the row's own task;
|
|
// Gladstone holds no Vikunja key, so Sage files it. Comments on 614 with the landing, 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-614.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 = 614;
|
|
const SIG = "Posted by Sage (T3 1ef1e4f8) for row 320 (Gladstone) with Sage's own key (bot user 13).";
|
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
const p = (...xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(SIG)}</p>`;
|
|
const TEXT = [
|
|
"Landed 094124b4e8e611f909bd76fb97cef370ac3891a2 on refactor at 21:53Z 10-23 (row 320, #1816, closed with comment 31413). Gladstone's candidate faf55618e was replayed by Sage onto 45e3ca7bd; code commits are patch-id equal, log appends went in one final commit.",
|
|
"F1-F6: hostcheck item 5 dpkg octal mode compare, run sheet bundle verify in scratch repos, case2-unit scoped Claude paths and unreadable cwd, precheck explicit notify decision, run sheet 9a notify dirs at 0700, wrapper step. Morgana APPROVE round 2 (Gitea comment 31403). Full landing gate GREEN 21:53:00Z, 0 failures.",
|
|
"Live stack is unchanged (45dca69f6). The 433 P1 rerun runs on this landing (mos-1532).",
|
|
];
|
|
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 || !/^433 P1 F1 F2: hostcheck item 5 statoverride mode compare/.test(task.json?.title ?? "")) throw new Error(`task ${TASK}: ${task.status}, project ${task.json?.project_id}, title isn't 614's`);
|
|
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);
|