From f81ebfbae9c50d7850c611f180785d289b09d461 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 04:15:29 -0500 Subject: [PATCH] sage: row 186 follow-ups filed as P19 405 and 406, P19 404 closed (mos-0955) --- .../vikunja-setup/close-404-2026-10-20.json | 20 ++++ agents/sage/work/vikunja-setup/close-404.mjs | 60 ++++++++++++ .../vikunja-setup/file-r186-followups.mjs | 91 +++++++++++++++++++ .../r186-followups-2026-10-20.json | 33 +++++++ 4 files changed, 204 insertions(+) create mode 100644 agents/sage/work/vikunja-setup/close-404-2026-10-20.json create mode 100644 agents/sage/work/vikunja-setup/close-404.mjs create mode 100644 agents/sage/work/vikunja-setup/file-r186-followups.mjs create mode 100644 agents/sage/work/vikunja-setup/r186-followups-2026-10-20.json diff --git a/agents/sage/work/vikunja-setup/close-404-2026-10-20.json b/agents/sage/work/vikunja-setup/close-404-2026-10-20.json new file mode 100644 index 000000000..483491424 --- /dev/null +++ b/agents/sage/work/vikunja-setup/close-404-2026-10-20.json @@ -0,0 +1,20 @@ +{ + "at": "2026-10-20T09:15:23.540Z", + "task": 404, + "title": "stack-release --plan backup line: N1 wrong on store-newer/migration-needs-flag runs, N2 TOOLS.md step 3 omits it", + "problems": [], + "comment": { + "status": 201, + "id": 356, + "readBack": 200, + "found": true + }, + "update": { + "status": 200, + "inverse": "mark undone: POST /api/v1/tasks/404 done false (sage key)", + "readBack": 200, + "done": true, + "titleSame": true, + "descriptionSame": true + } +} diff --git a/agents/sage/work/vikunja-setup/close-404.mjs b/agents/sage/work/vikunja-setup/close-404.mjs new file mode 100644 index 000000000..1e7e665d5 --- /dev/null +++ b/agents/sage/work/vikunja-setup/close-404.mjs @@ -0,0 +1,60 @@ +// Sage, 2026-10-20: row 186 landed (269134d8d, pushed in b367f5730..9760d4967, #1684), and mos-0955 split its two +// follow-ups into P19 405 and 406. Comments on P19 404 that it closes, 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-404.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 = 404; +const SIG = "Created by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13)."; +const esc = (s) => s.replace(/&/g, "&").replace(//g, ">"); +const p = (...xs) => xs.map((x) => `

${esc(x)}

`).join("") + `

${esc(SIG)}

`; +const TEXT = [ + "Closing: N1 and N2 landed with row 186 (269134d8d, comment 354). The two follow-ups from its review are P19 405 and 406 (comment 355), owner Sage, so nothing stays open here.", +]; +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-release --plan backup line/.test(task.json?.title ?? "")) throw new Error(`task ${TASK}: ${task.status}, project ${task.json?.project_id}, title isn't 404'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); diff --git a/agents/sage/work/vikunja-setup/file-r186-followups.mjs b/agents/sage/work/vikunja-setup/file-r186-followups.mjs new file mode 100644 index 000000000..33be8eeea --- /dev/null +++ b/agents/sage/work/vikunja-setup/file-r186-followups.mjs @@ -0,0 +1,91 @@ +// Sage, 2026-10-20: mos-0955 (file the two row 186 follow-ups as P19 tasks, one each, owner Sage, no due date; a +// comment on P19 404 doesn't count under Q109). Files two P19 tasks and comments on P19 404 with their numbers. +// Sage's own key (bot user 13); the sync key reads back. No labels, no assignees, no due date, no bucket moves; the +// owner is named in the description, as on 404. Refuses if a title exists. Rollback is marking a new task done; +// deletion is Jason's or ops-01's. Prints no secret. +// Usage: node file-r186-followups.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 PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13)."; +const esc = (s) => s.replace(/&/g, "&").replace(//g, ">"); +const ps = (sig, xs) => xs.map((x) => `

${esc(x)}

`).join("") + `

${esc(sig)}

`; +const TASKS = [ + { + title: "stack-release --plan: no test plans a missing store with an out-of-chain pointer schema (row 186 M6)", + description: ps(SIG, [ + "Filbert's N1 on row 186 (Gitea #1684 comment 29419, landing 269134d8d). Filbert's mutant M6 moves the early test (backup: none; the run rolls back before the backup) ahead of the no-store test in plan, and it survives the plan tests 18/18. No test plans a missing store whose pointer schema is outside the target's chain, or in it without --migrate. The landed order is right: with no store file the run skips the whole store block, so \"there is no store file\" is the true reason.", + "That same case's migration line already says the run would roll back with store-newer or migration-needs-flag, though the run takes neither path with no store. This predates row 185. One small row can fix the line and add the test that kills M6.", + "Owner Sage. No queue row until scheduled (mos-0955). Not live-affecting: the plan text only.", + ]), + }, + { + title: "stack-release --plan: backup line printed before a store-unclean or schema-unreadable rollback (row 186 reading)", + description: ps(SIG, [ + "Darkwing's reading outside the row 186 brief (agents/darkwing/work/queue-186/BUILD.md), ruled a bounded follow-up by Filbert (N2, Gitea #1684 comment 29419). With the bus stopped and the store unclean (a -wal or -shm beside it) or unreadable, plan_store falls back to the pointer's schema. The run then rolls back with store-unclean or schema-unreadable before backup_store, but the plan judges by the pointer's schema and can print the backup line.", + "Filbert's ruling for the fix: print a store-unclean form of the none line only when the plan saw the stopped store unclean. The plan's store schema line already says the store was not read and why, so no window has been misled.", + "Owner Sage. No queue row until scheduled (mos-0955). Can share a row with the M6 follow-up.", + ]), + }, +]; +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: [], comment404: 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 before = await existing(); +for (const T of TASKS) { + const clash = before.filter((t) => t.title === T.title); + if (clash.length) throw new Error(`exists: #${clash[0].id}; refusing`); +} +const t404 = await api("GET", `/api/v2/tasks/404`, undefined, sync); +if (t404.status !== 200 || t404.json?.project_id !== PROJECT || !/stack-release --plan backup line/.test(t404.json?.title ?? "")) throw new Error(`task 404: ${t404.status}, not the P19 404 task`); +writeFileSync(OUT, "", { flag: "wx" }); +for (const T of TASKS) { + const r = await api("PUT", `/api/v1/projects/${PROJECT}/tasks`, { title: T.title, description: T.description }, key); + const c = { title: T.title, status: r.status, id: r.json?.id ?? null, index: r.json?.index ?? null }; + rec.created.push(c); + if (r.status >= 300 || !Number.isSafeInteger(r.json?.id)) { rec.problems.push(`create: ${r.status} ${r.json?.message ?? ""}`); save(); continue; } + c.inverse = `mark done: POST /api/v1/tasks/${r.json.id} done true (sage key); delete is Jason's or ops-01's`; + const back = await api("GET", `/api/v2/tasks/${r.json.id}`, undefined, sync); + Object.assign(c, { readBack: back.status, done: back.json?.done, due: back.json?.due_date ?? null }); + if (back.status !== 200 || back.json?.title !== T.title || back.json?.project_id !== PROJECT || back.json?.done) rec.problems.push(`create ${r.json.id} read-back differs`); + save(); +} +const ids = rec.created.map((c) => c.id).filter(Number.isSafeInteger); +if (ids.length === TASKS.length) { + const body = ps(PSIG, [`The two row 186 follow-ups are now their own P19 tasks, owner Sage, no due date (mos-0955): #${ids[0]} (Filbert's M6, no test plans a missing store with an out-of-chain pointer schema) and #${ids[1]} (the backup line before a store-unclean or schema-unreadable rollback). This task's N1 and N2 are done with row 186's landing, 269134d8d.`]); + const c = await api("PUT", `/api/v1/tasks/404/comments`, { comment: body }, key); + rec.comment404 = { status: c.status, id: c.json?.id ?? null }; + if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) rec.problems.push(`comment 404: ${c.status} ${c.json?.message ?? ""}`); + else { + const back = await api("GET", `/api/v2/tasks/404/comments?per_page=50`, undefined, sync); + const found = list(back.json).find((x) => x.id === c.json.id); + Object.assign(rec.comment404, { readBack: back.status, found: Boolean(found) }); + if (back.status !== 200 || !found) rec.problems.push(`comment 404 read-back: ${back.status}`); + } +} +save(); +process.stdout.write(JSON.stringify(rec, null, 1) + "\n"); +if (rec.problems.length) process.exit(2); diff --git a/agents/sage/work/vikunja-setup/r186-followups-2026-10-20.json b/agents/sage/work/vikunja-setup/r186-followups-2026-10-20.json new file mode 100644 index 000000000..05adb2dff --- /dev/null +++ b/agents/sage/work/vikunja-setup/r186-followups-2026-10-20.json @@ -0,0 +1,33 @@ +{ + "at": "2026-10-20T09:14:57.147Z", + "project": 19, + "created": [ + { + "title": "stack-release --plan: no test plans a missing store with an out-of-chain pointer schema (row 186 M6)", + "status": 201, + "id": 405, + "index": 238, + "inverse": "mark done: POST /api/v1/tasks/405 done true (sage key); delete is Jason's or ops-01's", + "readBack": 200, + "done": false, + "due": "0001-01-01T00:00:00Z" + }, + { + "title": "stack-release --plan: backup line printed before a store-unclean or schema-unreadable rollback (row 186 reading)", + "status": 201, + "id": 406, + "index": 239, + "inverse": "mark done: POST /api/v1/tasks/406 done true (sage key); delete is Jason's or ops-01's", + "readBack": 200, + "done": false, + "due": "0001-01-01T00:00:00Z" + } + ], + "comment404": { + "status": 201, + "id": 355, + "readBack": 200, + "found": true + }, + "problems": [] +}