// Sage, 2026-10-11: row 62 gate, "one live read with descendants on, root #19, read-only". Runs the // candidate's own discover(), reconcile() and tick() against tasks.woltje.com with the sync key only, // a real Store and Broker in a scratch data root. startup() is not run: its scope probes send a PATCH, // a DELETE and a POST as role keys. This builds the root's view from the same two reads startup's // install check makes, and checks it the same way. The fetch guard refuses anything but GET, so no // write leaves this process. Also Darkwing's probe for leftTree(): a child's missing kanban view next // to the child project read. Records methods, paths, statuses, counts and ids; no titles, no token. // The business and tracker come from the candidate's bootConfig(), the host config's path to the bus. // Usage: node tick-62.mjs SRC_ROOT https://tasks.woltje.com BUSINESS_ID OUT_JSON import { readFileSync, writeFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const [SRC, ORIGIN, BIZ_ID, OUT] = process.argv.slice(2); if (ORIGIN !== 'https://tasks.woltje.com') throw new Error('tasks.woltje.com only'); if (!OUT || existsSync(OUT)) throw new Error('OUT_JSON missing or exists; refusing'); const load = (p) => import(join(SRC, p)); const { Store } = await load('packages/bus/src/store.mjs'); const { Broker } = await load('packages/bus/src/broker.mjs'); const { Credentials } = await load('packages/bus/src/credentials.mjs'); const { client, all } = await load('packages/tasks/src/vikunja.mjs'); const { BUCKETS } = await load('packages/tasks/src/startup.mjs'); const { tick, reconcile, WATCH_ONLY } = await load('packages/tasks/src/sync.mjs'); const { loadSystem, bootConfig } = await load('packages/cli/src/config.mjs'); const ROOT = 19, MISSING = 2147483647; const boot = bootConfig({ system: loadSystem(), businessId: BIZ_ID }); const name = BIZ_ID; const b = boot.businesses[name]; const tracker = boot.trackers?.[name] ?? null; if (tracker && (tracker.baseUrl !== ORIGIN || tracker.project !== ROOT)) throw new Error('host tracker config is not #19 on ' + ORIGIN); const syncRef = b.tracker.sync.credentials.vikunja; const requests = []; const guarded = async (url, init = {}) => { const method = init.method ?? 'GET'; const u = new URL(url); if (method !== 'GET' || u.origin !== ORIGIN) throw new Error(`refused ${method} ${u.origin}`); const r = await fetch(url, { ...init, redirect: 'error' }); requests.push({ method, path: u.pathname.replace(/^\/api\/v2/, ''), status: r.status }); return r; }; const root = mkdtempSync(join(tmpdir(), 'tick-62-')); const credentials = new Credentials({ references: { [`${name}/@sync`]: { vikunja: syncRef } }, dataRoot: root, env: {} }); const store = new Store(root); const broker = new Broker({ store, businesses: { [name]: b }, secretCheck: (v) => credentials.assertClean(v) }); const call = client({ baseUrl: ORIGIN, fetch: guarded }); const ctx = { business: name, project: ROOT, descendants: true, broker, credentials, clock: Date.now, call, per: 50, view: null, tree: null, bots: new Set([b.tracker.sync.botId, ...Object.values(b.roles).map((r) => r.tracker?.botId).filter(Boolean)]), sync: (method, path, opts) => credentials.use(`${name}/@sync`, 'vikunja', (token) => call(token, method, path, opts)), lastStart: null, lastReconcile: null, lastComment: new Map(), commentsAt: new Map(), commentCount: new Map(), }; ctx.syncCall = (_t, method, path, opts) => ctx.sync(method, path, opts); const rec = { at: new Date().toISOString(), src: SRC, key: 'sync (bot 10)', root: ROOT, hostTracker: tracker && { project: tracker.project, descendants: tracker.descendants ?? null } }; try { const info = await ctx.call(null, 'GET', '/info'); rec.version = info.json?.version; // The root's install check, as startup.mjs inspect() makes it. const views = await all(ctx.syncCall, null, `/projects/${ROOT}/views`, {}, ctx.per); const kanban = views.items.filter((x) => x?.view_kind === 'kanban'); if (kanban.length !== 1 || kanban[0].bucket_configuration_mode !== 'manual') throw new Error('root kanban check'); const k = kanban[0]; const buckets = await all(ctx.syncCall, null, `/projects/${ROOT}/views/${k.id}/buckets`, {}, ctx.per); const ids = {}; for (const t of BUCKETS) { const hits = buckets.items.filter((x) => x?.title === t); if (hits.length !== 1) throw new Error('root bucket check ' + t); ids[t] = hits[0].id; } if (k.done_bucket_id !== ids.done || k.default_bucket_id !== ids.todo) throw new Error('root done/default check'); ctx.view = { id: k.id, done: ids.done, todo: ids.todo, ids, titles: Object.fromEntries(Object.entries(ids).map(([t, id]) => [id, t])) }; ctx.tree = new Map([[ROOT, ctx.view]]); rec.rootView = { id: k.id, buckets: ids }; const step = async (label, fn) => { const from = requests.length; const t0 = Date.now(); const result = await fn(); rec[label] = { result, ms: Date.now() - t0, gets: requests.length - from, requests: requests.slice(from).map((r) => `${r.status} ${r.method} ${r.path}`) }; }; await step('reconcile', () => reconcile(ctx)); rec.tree = [...ctx.tree].map(([p, v]) => ({ project: p, view: v ? v.id : 'watch-only' })); await step('tick', () => tick(ctx)); const current = broker.taskView(name, 'current'); const byProject = {}; for (const x of current) { const p = x.task_ref.split(':')[1].split('/')[0]; byProject[p] ??= { tasks: 0, watchOnly: 0, done: 0 }; byProject[p].tasks++; if (x.fields.bucket === WATCH_ONLY) byProject[p].watchOnly++; if (x.fields.done) byProject[p].done++; } rec.current = byProject; rec.open = broker.taskView(name, 'open').length; // leftTree(): what a child's board read gets when its kanban view is gone, next to the project read. const child = 18; const gone = await ctx.sync('GET', `/projects/${child}/views/${MISSING}/buckets/tasks`, { query: { filter: 'done = false', per_page: 50, page: 1 } }); const proj = await ctx.sync('GET', `/projects/${child}`); rec.deletedViewProbe = { child, view: MISSING, board: { status: gone.status, code: gone.json?.code ?? null }, project: { status: proj.status } }; rec.writes = requests.filter((r) => r.method !== 'GET').length; } catch (e) { rec.error = String(e?.code ?? e?.message ?? e); } finally { store.close(); credentials.close(); rmSync(root, { recursive: true, force: true }); } const out = JSON.stringify(rec, null, 1) + '\n'; const token = readFileSync(syncRef.file, 'utf8').trim(); if (!token || out.includes(token) || /bearer/i.test(out)) throw new Error('secret check failed; not written'); writeFileSync(OUT, out, { flag: 'wx' }); process.stdout.write(out); process.exitCode = rec.error ? 1 : 0;