decisions, build-log, sessions: 110, Q109 follow-ups filed in Vikunja P19 (#153-#161)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-12 12:38:43 -05:00
co-authored by Claude Opus 5.5
parent 51711d60ad
commit c444c3ae3f
5 changed files with 211 additions and 0 deletions
+6
View File
@@ -4388,3 +4388,9 @@ After (1a8ba587): Dewey's round 1, 4 files +202 -22. render and install take `--
Before: mosaic.woltje.com served the old build, and the Console unit (installed 04:14:24Z from 777ada59) was installed for mosaic-preview.woltje.com, disabled and inactive.
After: Jason gave the go at 17:21:09Z. Mos reports that ops-01 switched Argo `mosaic-preview` to `overlays/cutover` (Synced and Healthy at ebfd7065 from 17:21:32Z). Mos reinstalled the unit for mosaic.woltje.com under decision 103 and started it at 17:21:41Z, PID 3951396, from 6040d424, which has no webui diff from 36e34e5b. Their checks: anonymous LAN and external requests and forged headers get 302 to Authentik. Directly through the tunnel, the Console returns 401 with no identity and 403 for a non-local Host (ops-01, 17:22Z). Jason reached the dashboard from the WAN. Sage's read-only check at 17:27Z: `scripts/webui-service.sh status` enabled, active, NRestarts 0, "send: on", /healthz 200; ExecStart is decision 103's command. Undo: the Argo path back to `overlays/prod` and a reinstall with the preview host. Found while checking: the unit runs from the canonical checkout, and each request spawns human-cli from it, so landings there are live changes. Decision 109 sets the landing rule until a follow-up row pins the Console's copy.
### 2026-10-12 — Sage, follow-ups move to Vikunja (Q109, decision 110)
Before: the follow-ups from rows 76, 83, 67 and the I1 review existed only as lists in BUILD-LOG entries, the standup and decision 109. Mos had filed #146 (decision 109's pinned Console copy) and #147 (decision 101's uid reinstall) in Vikunja project 19, both due 2026-10-13.
After: Jason ruled "Always use Vikunja for task tracking" (Q109, relayed by Mos in sage-0081). At 17:37:55Z Sage filed nine tasks in project 19 on tasks.woltje.com with the pm token slot, each read back with the sync key: #153 to #155 (the I1, M1 and row 77 briefs, due 2026-10-13) and #156 to #161 (the open follow-ups). Evidence: `agents/sage/work/vikunja-setup/2026-10-12_followups.json` and `file-followups.mjs`. The follow-up lists in the row 83 and cutover entries above are now tracked as #159, #160 and #146. Later entries cite Vikunja ids instead of listing follow-ups.
@@ -0,0 +1,97 @@
{
"at": "2026-10-12T17:37:55.750Z",
"project": 19,
"created": [
{
"title": "I1 brief: containerize managed sessions by role (decisions 105-108)",
"id": 153,
"index": 39,
"inverse": "mark done: POST /api/v1/tasks/153 done true (pm); delete is Jason's or ops-01's",
"readBack": 200,
"project_back": 19,
"due_back": "2026-10-13T22:00:00Z",
"done": false
},
{
"title": "M1 brief and queue row: chief-of-staff role (decision 104)",
"id": 154,
"index": 40,
"inverse": "mark done: POST /api/v1/tasks/154 done true (pm); delete is Jason's or ops-01's",
"readBack": 200,
"project_back": 19,
"due_back": "2026-10-13T22:00:00Z",
"done": false
},
{
"title": "Row 77 brief: Console service hardening (#1571)",
"id": 155,
"index": 41,
"inverse": "mark done: POST /api/v1/tasks/155 done true (pm); delete is Jason's or ops-01's",
"readBack": 200,
"project_back": 19,
"due_back": "2026-10-13T22:00:00Z",
"done": false
},
{
"title": "Narrow the pi worker's dataRoot mount (compose.yaml:40)",
"id": 156,
"index": 42,
"inverse": "mark done: POST /api/v1/tasks/156 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
},
{
"title": "Stale docs: LAYERS.md L4 and CONDUCTOR.md lines 17 and 40",
"id": 157,
"index": 43,
"inverse": "mark done: POST /api/v1/tasks/157 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
},
{
"title": "Board Reply needs a delivery path that isn't tmux",
"id": 158,
"index": 44,
"inverse": "mark done: POST /api/v1/tasks/158 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
},
{
"title": "Console cosmetic follow-ups from rows 76 and 83",
"id": 159,
"index": 45,
"inverse": "mark done: POST /api/v1/tasks/159 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
},
{
"title": "Console flag tests: allowed user starting with \"-\", and the [--send] usage text",
"id": 160,
"index": 46,
"inverse": "mark done: POST /api/v1/tasks/160 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
},
{
"title": "Row 67 test 4: assert its new decisions are listed (Dewey N1)",
"id": 161,
"index": 47,
"inverse": "mark done: POST /api/v1/tasks/161 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,82 @@
// Sage, 2026-10-12: Q109 ("Always use Vikunja for task tracking"). Files Sage's open Mosaic follow-ups
// as tasks in project 19 with the pm token slot, the way Mos filed #146 to #152. Refuses if any title
// already exists in project 19. The sync key reads each task 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-followups.mjs https://tasks.woltje.com PM_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, DUE = "2026-10-13T22:00:00Z";
const SIG = "Created by Sage (T3 1ef1e4f8) with the pm token slot until Sage has its own key.";
const p = (...xs) => xs.map((x) => `<p>${x}</p>`).join("") + `<p>${SIG}</p>`;
const TASKS = [
{ title: "I1 brief: containerize managed sessions by role (decisions 105-108)", due: DUE,
description: p("Sage writes the I1 brief on BRIEF-TEMPLATE.md from agents/sage/work/next-target/I1-review.md: two broker sockets with the human one only in the Console, a new via value in authenticateHuman, narrow per-run mounts, cap_drop ALL and limits, an internal network with one egress proxy, the rootless endpoint pinned with a refusal unless docker info lists name=rootless, and the subscription token as a read-only file secret. The survey's unverified items go in as spikes with tests. Owner Sage.") },
{ title: "M1 brief and queue row: chief-of-staff role (decision 104)", due: DUE,
description: p("Move agents/sage/work/next-target/M1-draft.md to docs/plans on BRIEF-TEMPLATE.md and open its queue row: owner Rocko, reviewers Darkwing and Filbert, gate Sage. The row ships roles/chief-of-staff.json and .md, fixture tests in packages/business/tests and the documented instance entry; it never writes Jason's business file. The instance name is #150. Owner Sage.") },
{ title: "Row 77 brief: Console service hardening (#1571)", due: DUE,
description: p("Write the brief for queue row 77 (Gitea #1571, owner Rocko), held until after the cutover, which is done (#152). Coordinate with #146 and #147, since all three change how the mosaic-webui unit runs. Owner Sage.") },
{ title: "Narrow the pi worker's dataRoot mount (compose.yaml:40)",
description: p("compose.yaml:40 mounts the whole dataRoot read-write into the pi worker as uid 1000, which includes bus/broker.sock. Narrow it to the worker's own run and workspace paths. This exposure exists today and doesn't wait on I1. Found in Sage's I1 review. Needs a queue row and brief.") },
{ title: "Stale docs: LAYERS.md L4 and CONDUCTOR.md lines 17 and 40",
description: p("LAYERS.md L4 and docs/plans/CONDUCTOR.md:17,40 say managed sessions run in containers. They run on the host today. Correct the docs, and point at I1 for the containerized design. Found in Sage's I1 review.") },
{ title: "Board Reply needs a delivery path that isn't tmux",
description: p("The control board's /api/reply pastes into a tmux pane, which can't cross a container boundary. It needs another delivery path before the board or the sessions containerize under I1. Found in Sage's I1 review.") },
{ title: "Console cosmetic follow-ups from rows 76 and 83",
description: p("Small fixes left after the cutover:",
"1. packages/webui/systemd/mosaic-webui.service.in lines 6-9 still say Jason starts the unit and cite decisions 62 and 94; decision 102 says Jason or Mos.",
"2. packages/webui/README.md line 106 is 102 characters; wrap it.",
"3. Row 76: Darkwing's spliced header-comment sentence at packages/webui/tests/send-browser.test.mjs line 4, and Filbert's two round 2 notes (Gitea #1570 comment 27685).",
"4. When a session has expired, the proxy redirects a Reply to sign-in, the fetch fails without a status, and the page says \"Delivery may be unknown\" (packages/webui/src/public/app.js:380). Nothing reached the Console, so it should say not sent and to sign in again.",
"Touches packages/webui, so decision 109's notice to Mos applies before landing.") },
{ title: "Console flag tests: allowed user starting with \"-\", and the [--send] usage text",
description: p("From the row 83 landing (1a8ba587): an --allow-user value beginning with \"-\" needs a decision and a test in both scripts/webui-service.sh and packages/webui/src/serve.mjs, kept consistent. Darkwing's review (#1576 comment 27721) left a surviving mutant on the [--send] usage text; add a test that kills it. Touches packages/webui, so decision 109's notice to Mos applies before landing.") },
{ title: "Row 67 test 4: assert its new decisions are listed (Dewey N1)",
description: p("Dewey's N1 on row 67 (#1560 comment 27452), which Darkwing repeated (27627): test 4 creates decisions but doesn't assert that the listing shows them. Add the assertion.") },
];
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);
+1
View File
@@ -752,3 +752,4 @@ are never rewritten or removed; corrections are new entries.
2026-10-12T14:59:20Z | sage | decision 107, I1-3 deferred | Jason deferred Q104 via Mos sage-0077: no WebAuthn in the I1 brief; reopens only as a new question once containers run; a compromised Console holding human authority is recorded as open
2026-10-12T15:00:59Z | sage | decision 108, I1-4 answered | Jason chose Q105 A via Mos sage-0078: subscription token as a read-only file secret per session container, no API key or gateway; copy-out risk accepted, bounded by the egress allowlist; all four I1 items answered, brief is Sage's on 2026-10-13
2026-10-12T17:27:49Z | sage | cutover live, decision 109 | verified the mosaic.woltje.com Console read-only after Mos's 17:21Z start (status active, send on, healthz 200); decision 109: landings touching webui, bus or cli in the canonical checkout are live changes until the Console runs from a pinned copy; BUILD-LOG entry
2026-10-12T17:38:19Z | sage | decision 110, follow-ups to Vikunja | Jason's Q109 via Mos sage-0081: Vikunja project 19 holds Mosaic work items; filed #153-#161 with the pm slot, read back with sync; #146 and #147 cited for the 10-13 rows; queue and Gitea stay the review and landing record; BUILD-LOG entry
+25
View File
@@ -2437,3 +2437,28 @@ which stay with him. Each item names who decided it and what happened.
being deploys. Pointing the unit at it is a reinstall, which is
Mos's step under decisions 102 and 103. It goes in the same window
as decision 101's uid reinstall.
110. **Vikunja holds Mosaic work items (2026-10-12, Q109).** Jason, relayed
by Mos in sage-0081: "Always use Vikunja for task tracking." Mosaic
work items live as tasks in project 19 (Mosaic Stack) on
tasks.woltje.com. Sage files each new one there when raising it and
stops keeping follow-up lists in BUILD-LOG, standups or decisions.
Those records cite the Vikunja id.
- Filed at 17:37:55Z with the pm token slot, as Mos filed #146 to
#152, since Sage has no key of its own: #153 I1 brief, #154 M1
brief and row, #155 row 77 brief (all three due 2026-10-13), #156
compose.yaml:40 mount, #157 stale LAYERS and CONDUCTOR docs, #158
board Reply without tmux, #159 Console cosmetic notes from rows 76
and 83, #160 Console flag tests, #161 row 67 test 4 (Dewey N1).
Each was read back with the sync key. No labels, assignees or
bucket moves. Evidence:
`agents/sage/work/vikunja-setup/2026-10-12_followups.json`.
- Decision 109's pinned-copy row is #146 and decision 101's uid
reinstall is #147. Both are due 2026-10-13, and the 10-13 rows
cite them.
- The queue and its Gitea issues stay the review and landing
record, because `queue review` and `verify-commit` gate landings
on them. When a row opens for a Vikunja task, the task names the
row and the row's brief names the task. Moving the queue itself
into Vikunja would be a separate change and goes to Jason first.
- If the pm slot fails for Sage, Mos files the task (sage-0081).
Agent key scopes are #151, owned by ops-01.