logs, vikunja-setup: row 69 landed, follow-up filed as Vikunja #243 (#1562)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-12 18:40:27 -05:00
co-authored by Claude Opus 5.5
parent 7f0f54ab9d
commit c974cafa4f
4 changed files with 90 additions and 0 deletions
@@ -0,0 +1,17 @@
{
"at": "2026-10-12T23:38:01.305Z",
"project": 19,
"created": [
{
"title": "Terminal app follow-up from row 69: help pointer, pending-resolve box test, statusStyle",
"id": 243,
"index": 90,
"inverse": "mark done: POST /api/v1/tasks/243 done true (pm); delete is Jason's or ops-01's",
"readBack": 200,
"project_back": 19,
"due_back": "0001-01-01T00:00:00Z",
"done": false
}
],
"problems": []
}
@@ -0,0 +1,66 @@
// Sage, 2026-10-12: decision 110. Files row 69's non-blocking follow-ups (#1562) as one task in project 19
// with Sage's own key (bot user 13). Refuses if the title already exists. The sync key reads it back. No
// labels, no assignees, no bucket moves. Rollback is marking the task done; deletion is Jason's or ops-01's.
// Prints no secret.
// Usage: node file-r69-followup.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 PROJECT = 19;
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 TASKS = [
{ title: "Terminal app follow-up from row 69: help pointer, pending-resolve box test, statusStyle",
description: p("Non-blocking items left when row 69 landed (90d7ed66, Gitea #1562 comment 28061). All in packages/cli; decision 109's notice to Mos applies before landing.",
"1. The cut help-pointer line (Filbert, #1562 comment 28055).",
"2. No test hides the box while a resolve is pending. Dewey's two surviving mutants (#1562 comment 28048) show it: the status swap and the way-out line each apply only while o.asking, and no test catches either one being dropped.",
"3. Dewey's statusStyle survivor: a warning style changed to muted is not caught.",
"Needs a queue row and a brief section before anyone builds it.") },
];
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 : []);
async function existing() {
const out = [];
for (let page = 1; page < 20; page++) {
const r = await api("GET", `/api/v2/projects/${PROJECT}/tasks?per_page=50&page=${page}`, undefined, sync);
if (r.status !== 200) throw new Error(`list tasks: ${r.status}`);
out.push(...list(r.json));
if (list(r.json).length < 50) return out;
}
throw new Error("more than 19 pages");
}
const rec = { at: new Date().toISOString(), project: PROJECT, created: [], 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 before = await existing();
const clash = before.filter((t) => TASKS.some((x) => x.title === t.title));
if (clash.length) throw new Error(`exists: ${clash.map((t) => `#${t.id} ${t.title}`).join("; ")}; refusing`);
writeFileSync(OUT, "", { flag: "wx" });
for (const t of TASKS) {
rec.created.push({ title: t.title });
save();
const body = { title: t.title, description: t.description, ...(t.due ? { due_date: t.due } : {}) };
const r = await api("PUT", `/api/v1/projects/${PROJECT}/tasks`, body, key);
if (r.status >= 300 || !Number.isSafeInteger(r.json?.id)) {
rec.problems.push(`create ${t.title}: ${r.status} ${r.json?.code ?? ""} ${r.json?.message ?? ""}`);
break;
}
Object.assign(rec.created.at(-1), { id: r.json.id, index: r.json.index, inverse: `mark done: POST /api/v1/tasks/${r.json.id} done true (pm); delete is Jason's or ops-01's` });
const back = await api("GET", `/api/v2/tasks/${r.json.id}`, undefined, sync);
Object.assign(rec.created.at(-1), { readBack: back.status, project_back: back.json?.project_id, due_back: back.json?.due_date, done: back.json?.done });
if (back.status !== 200 || back.json?.title !== t.title || back.json?.project_id !== PROJECT || back.json?.done) rec.problems.push(`${t.title} read-back differs`);
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);