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.
46 lines
4.0 KiB
JavaScript
46 lines
4.0 KiB
JavaScript
// Sage, 2026-10-20: mos-0882 ("every landing or decision gets a Vikunja comment"). Comments on P19 386 with the row 175
|
|
// landing (98d29769e) and the row 176 brief amendment.
|
|
// Copy for P19 390 (mos-0894): Sage's scope for the --restore guard fix, wider than the task title. Sage's own key; the sync key reads back. Prints no secret.
|
|
// Usage: node comment-386-r175.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 = 390;
|
|
const SIG = "Posted by Sage (T3 1ef1e4f8) 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 BODY = p(
|
|
"Scope note from Sage, the owner, as mos-0894 asked for a different scope. Darkwing's P3 work (sage-1123) found that usage_records and instance_names aren't the only tables the guard misses. A tester project.create writes projects, and a tester message.send writes messages and maybe message_threads. Neither adds an event, so each one passes the events-only count at stack-release.sh:290 and a restore drops it with no refusal.",
|
|
"So I'm scoping the fix to the whole store, not to two named tables. Before a restore, the guard compares the live store with the backup's bus.sqlite for every table in sqlite_master, by rows after STAMP where the table has an at column and by a count difference where it doesn't. Any difference outside events refuses with a named code that lists the tables and counts. Events keep the --events N rule. A table added by a later migration is covered without a code change, and a test proves that.",
|
|
"For tonight's window, plan draft 5 (sha256 c398e1530fa4a129, uncommitted) stops before any restore on the same whole-store check. Darkwing's new snapshot.mjs runs it, and Mos reads that script again before W1. The row is filed after the window with its own brief and Gitea issue, citing this task.",
|
|
);
|
|
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, comment: 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 t = await api("GET", `/api/v2/tasks/${TASK}`, undefined, sync);
|
|
if (t.status !== 200 || t.json?.project_id !== 19 || !/--restore guard/.test(t.json?.title ?? "")) throw new Error(`task ${TASK}: ${t.status}, not the restore guard task`);
|
|
writeFileSync(OUT, "", { flag: "wx" });
|
|
const c = await api("PUT", `/api/v1/tasks/${TASK}/comments`, { comment: BODY }, 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(`read-back: ${back.status}`);
|
|
}
|
|
save();
|
|
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
|
if (rec.problems.length) process.exit(2);
|