Files
jason.woltje 8545057275 lead: commit sage's untracked evidence (100 files: prd-1-17 queue-135 queue-176 queue-199 queue-66 vikunja-setup window-repin-4429a104f)
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.
2026-11-02 15:05:15 -06:00

62 lines
4.9 KiB
JavaScript

// Sage, 2026-10-24: Vikunja 623 (Sage's own task, mos-1598) landed 06f507dbc on refactor. Comments on 623 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-623.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 = 623;
const SIG = "Posted 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 = [
"Landed 06f507dbc07eacb35659eb9a8c3aaca1635b63d5 on refactor at 18:55Z 10-24, doc only. docs/plans/BRIEF-TEMPLATE.md Gate section: make TMPDIR with shell mktemp -d /mnt/storage/scratch/tmp/XXXXXX only (31 bytes, letters and digits), never Python tempfile.mkdtemp (underscore, 48 bytes, fails claude-session.test.mjs:298 and the 107-byte tools.sock limit), one TMPDIR per run. Set nice absolutely: nice -n $((19 - $(nice))) in shell, 19 in each child's preexec in a launcher, and record the nice each run saw in BUILD.md.",
"Gate scripts: Sage's landing gate script now re-execs at nice 19 and logs TMPDIR and nice at start. scripts/testing/gate-lane.mjs already makes 30-byte mkdtempSync roots (ga-XXXXXX, gLANE-XXXXXX) with no underscore; it sets no nice, and it isn't adopted for gates yet. Adding nice there is code and goes in the row that adopts the lanes.",
"Fethry's row 328 runs already followed it: 801 shell gXXX TMPDIRs at 29 bytes, nice 19 set per child, observed values recorded.",
];
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 || !/^Stack: pin builder and gate TMPDIR to a short alphanumeric mktemp/.test(task.json?.title ?? "")) throw new Error(`task ${TASK}: ${task.status}, project ${task.json?.project_id}, title isn't 623'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);