@bongos/core 1.19.337 → 1.19.338

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,8 @@
27
27
  const auth = require('./auth');
28
28
  const db = require('./db');
29
29
  const ic = require('../../src/module-api');
30
+ // task 1003208: structured logging (pino via the doorway) — was console.*.
31
+ const log = ic.logger('discord');
30
32
 
31
33
  let client = null; // the live discord.js Client (or null when disabled/down)
32
34
  let starting = false; // guard against double-start
@@ -165,7 +167,7 @@ function makeInboundChannelHandler({ channelId, aggregator, logLabel }) {
165
167
  messageId: msg.id,
166
168
  });
167
169
  } catch (err) {
168
- console.error(`[discord-bot] ${logLabel} error:`, err.message);
170
+ log.error(`[discord-bot] ${logLabel} error:`, err.message);
169
171
  }
170
172
  };
171
173
  }
@@ -192,11 +194,11 @@ function getClient() {
192
194
  // boot) can't be taken down by Discord.
193
195
  async function start() {
194
196
  if (!botEnabled()) {
195
- console.log('[discord-bot] disabled (DISCORD_BOT_ENABLED is not "true") — not connecting.');
197
+ log.info('[discord-bot] disabled (DISCORD_BOT_ENABLED is not "true") — not connecting.');
196
198
  return null;
197
199
  }
198
200
  if (client || starting) {
199
- console.log('[discord-bot] start() called but a client is already active — skipping.');
201
+ log.info('[discord-bot] start() called but a client is already active — skipping.');
200
202
  return client;
201
203
  }
202
204
 
@@ -204,7 +206,7 @@ async function start() {
204
206
  if (!token) {
205
207
  // Enabled but no token: a misconfiguration, not a crash. Log loudly and
206
208
  // bail so the rest of the server still boots.
207
- console.error('[discord-bot] DISCORD_BOT_ENABLED is on but no bot token is configured '
209
+ log.error('[discord-bot] DISCORD_BOT_ENABLED is on but no bot token is configured '
208
210
  + '(set DISCORD_BOT_TOKEN or bot_token in ~/.config/otb/discord-bot.json) — not connecting.');
209
211
  return null;
210
212
  }
@@ -240,10 +242,10 @@ async function start() {
240
242
  c.once(Events.ClientReady, (ready) => {
241
243
  const guilds = ready.guilds?.cache?.size ?? 0;
242
244
  // ready.user.tag is the bot's name#id — safe to log; the TOKEN never is.
243
- console.log(`[discord-bot] connected as ${ready.user.tag} — in ${guilds} guild(s).`);
245
+ log.info(`[discord-bot] connected as ${ready.user.tag} — in ${guilds} guild(s).`);
244
246
  if (guilds === 0) {
245
247
  const worldName = (() => { try { return ic.branding().identity?.worldName || 'project'; } catch { return 'project'; } })();
246
- console.log(`[discord-bot] note: bot is in 0 guilds. Invite it to the ${worldName} `
248
+ log.info(`[discord-bot] note: bot is in 0 guilds. Invite it to the ${worldName} `
247
249
  + 'server (F5/role-sync needs it there with Manage Roles) before role/reaction features can act.');
248
250
  }
249
251
  // Ensure all managed rank roles exist in the guild — creates any missing
@@ -254,7 +256,7 @@ async function start() {
254
256
  ? ready.guilds?.cache?.get(roleSync.loadGuildId()) || null
255
257
  : ready.guilds?.cache?.first() || null;
256
258
  roleSync.ensureRankRoles(guild).catch((e) =>
257
- console.error('[discord-bot] ensureRankRoles failed:', e.message));
259
+ log.error('[discord-bot] ensureRankRoles failed:', e.message));
258
260
 
259
261
  // F5 role-sync (ADR 0033 §4): an initial reconcile once connected, then a
260
262
  // periodic sweep. Both best-effort — a Discord perms/hierarchy problem
@@ -278,21 +280,21 @@ async function start() {
278
280
  // discord.js auto-reconnects transient gateway drops on its own; we only
279
281
  // LOG the lifecycle so an operator can see the connection's health in the
280
282
  // journal. No custom reconnect loop — that would fight the library.
281
- c.on(Events.Error, (err) => console.error('[discord-bot] client error:', err.message));
282
- c.on(Events.Warn, (msg) => console.warn('[discord-bot] warning:', msg));
283
- c.on(Events.ShardError, (err) => console.error('[discord-bot] shard error:', err.message));
283
+ c.on(Events.Error, (err) => log.error('[discord-bot] client error:', err.message));
284
+ c.on(Events.Warn, (msg) => log.warn('[discord-bot] warning:', msg));
285
+ c.on(Events.ShardError, (err) => log.error('[discord-bot] shard error:', err.message));
284
286
  c.on(Events.ShardDisconnect, (event, id) =>
285
- console.warn(`[discord-bot] shard ${id} disconnected (code ${event?.code}) — discord.js will attempt to reconnect.`));
287
+ log.warn(`[discord-bot] shard ${id} disconnected (code ${event?.code}) — discord.js will attempt to reconnect.`));
286
288
  c.on(Events.ShardReconnecting, (id) =>
287
- console.log(`[discord-bot] shard ${id} reconnecting…`));
289
+ log.info(`[discord-bot] shard ${id} reconnecting…`));
288
290
  c.on(Events.ShardResume, (id) =>
289
- console.log(`[discord-bot] shard ${id} resumed.`));
291
+ log.info(`[discord-bot] shard ${id} resumed.`));
290
292
  // 'invalidated' = the session can't be recovered (e.g. token revoked).
291
293
  // discord.js will NOT auto-reconnect; reconnecting would hammer the API.
292
294
  // Log loudly, tear down, and leave it for a human — a revoked token is an
293
295
  // operator action, not a transient blip.
294
296
  c.on(Events.Invalidated, () => {
295
- console.error('[discord-bot] session invalidated (token revoked or auth failed) — '
297
+ log.error('[discord-bot] session invalidated (token revoked or auth failed) — '
296
298
  + 'destroying client; a human must re-check the token and restart.');
297
299
  stop().catch(() => {});
298
300
  });
@@ -303,7 +305,7 @@ async function start() {
303
305
  // event loop on every 👍 guild-wide (F4 efficiency finding). Config is
304
306
  // boot-time anyway (changing it needs a restart, like the kill-switch).
305
307
  const shipFeedChannelId = loadShipFeedChannelId();
306
- console.log(shipFeedChannelId
308
+ log.info(shipFeedChannelId
307
309
  ? `[discord-bot] watching #ship-feed (${shipFeedChannelId}) for 👍 upvotes.`
308
310
  : '[discord-bot] no DISCORD_SHIP_FEED_CHANNEL_ID set — upvoting is inert (no channel pinned).');
309
311
 
@@ -311,14 +313,14 @@ async function start() {
311
313
  // builder). Resolved once at startup; null → the approval surface is inert
312
314
  // (fail-closed — the bot won't act on reactions in arbitrary channels).
313
315
  const memberApprovalsChannelId = loadMemberApprovalsChannelId();
314
- console.log(memberApprovalsChannelId
316
+ log.info(memberApprovalsChannelId
315
317
  ? `[discord-bot] watching #member-approvals (${memberApprovalsChannelId}) for Archon 👍/👎.`
316
318
  : '[discord-bot] no DISCORD_MEMBER_APPROVALS_CHANNEL_ID set — member approvals are inert (no channel pinned).');
317
319
 
318
320
  // F6 inbound: the #ideas channel whose messages become idea_inbox entries.
319
321
  // Resolved once at startup (config is boot-time). Null → inbound is inert.
320
322
  const ideasChannelId = loadIdeasChannelId();
321
- console.log(ideasChannelId
323
+ log.info(ideasChannelId
322
324
  ? `[discord-bot] watching #ideas (${ideasChannelId}) for inbound ideas.`
323
325
  : '[discord-bot] no DISCORD_IDEAS_CHANNEL_ID set — inbound idea filing is inert (no channel pinned).');
324
326
 
@@ -328,7 +330,7 @@ async function start() {
328
330
  // startup; onFlush is the real filing path (recordInboundIdea).
329
331
  const ideaAggregator = inbound.createIdeaAggregator({
330
332
  onFlush: (combined) => inbound.recordInboundIdea(combined).then((result) => {
331
- console.log(`[discord-bot] #ideas burst → ${result.outcome}${result.ideaId ? ` (idea #${result.ideaId}, ${result.images || 0} img)` : (result.reason ? ` (${result.reason})` : '')}`);
333
+ log.info(`[discord-bot] #ideas burst → ${result.outcome}${result.ideaId ? ` (idea #${result.ideaId}, ${result.images || 0} img)` : (result.reason ? ` (${result.reason})` : '')}`);
332
334
  return result;
333
335
  }),
334
336
  });
@@ -337,7 +339,7 @@ async function start() {
337
339
  // tasks (kind='bug') straight-through, under the anti-abuse guard in
338
340
  // discord-bugs.js. Resolved once at startup. Null → bug intake is inert.
339
341
  const bugsChannelId = loadBugsChannelId();
340
- console.log(bugsChannelId
342
+ log.info(bugsChannelId
341
343
  ? `[discord-bot] watching #bugs (${bugsChannelId}) for inbound bug-tasks.`
342
344
  : '[discord-bot] no DISCORD_BUGS_CHANNEL_ID set — inbound bug filing is inert (no channel pinned).');
343
345
 
@@ -346,7 +348,7 @@ async function start() {
346
348
  // real filing path (recordInboundBug). Built once at startup.
347
349
  const bugAggregator = bugs.createBugAggregator({
348
350
  onFlush: (combined) => bugs.recordInboundBug(combined).then((result) => {
349
- console.log(`[discord-bot] #bugs burst → ${result.outcome}${result.taskId ? ` (task #${result.taskId}, ${result.status}, ${result.images || 0} img)` : (result.reason ? ` (${result.reason})` : '')}`);
351
+ log.info(`[discord-bot] #bugs burst → ${result.outcome}${result.taskId ? ` (task #${result.taskId}, ${result.status}, ${result.images || 0} img)` : (result.reason ? ` (${result.reason})` : '')}`);
350
352
  return result;
351
353
  }),
352
354
  });
@@ -385,9 +387,9 @@ async function start() {
385
387
  // systemd journal is less access-controlled than audit_log (Archon-only),
386
388
  // which already records the id with surface='discord_bot'. Log only the
387
389
  // task + outcome here (Hacker finding).
388
- console.log(`[discord-bot] 👍 on task #${taskId} → ${result.outcome}`);
390
+ log.info(`[discord-bot] 👍 on task #${taskId} → ${result.outcome}`);
389
391
  } catch (err) {
390
- console.error('[discord-bot] reaction handler error:', err.message);
392
+ log.error('[discord-bot] reaction handler error:', err.message);
391
393
  }
392
394
  });
393
395
 
@@ -438,9 +440,9 @@ async function start() {
438
440
  }
439
441
  // Don't log the reacting Discord id (audit_log already records it,
440
442
  // Archon-only). Log only the record + outcome.
441
- console.log(`[discord-bot] approval ${kind} #${marker.id} (${decision}) → ${result.outcome}`);
443
+ log.info(`[discord-bot] approval ${kind} #${marker.id} (${decision}) → ${result.outcome}`);
442
444
  } catch (err) {
443
- console.error('[discord-bot] approval handler error:', err.message);
445
+ log.error('[discord-bot] approval handler error:', err.message);
444
446
  }
445
447
  });
446
448
 
@@ -478,9 +480,9 @@ async function start() {
478
480
  if (!builder) return; // unlinked arrival — nothing to sync
479
481
  const roleSync = require('./discord-role-sync');
480
482
  const r = await roleSync.syncBuilderRole(builder.id);
481
- console.log(`[discord-bot] member joined → role-sync for builder ${builder.id}: ${r.ok ? (r.noop ? 'noop' : 'synced') : r.reason}`);
483
+ log.info(`[discord-bot] member joined → role-sync for builder ${builder.id}: ${r.ok ? (r.noop ? 'noop' : 'synced') : r.reason}`);
482
484
  } catch (err) {
483
- console.error('[discord-bot] member-add handler error:', err.message);
485
+ log.error('[discord-bot] member-add handler error:', err.message);
484
486
  }
485
487
  });
486
488
 
@@ -488,14 +490,14 @@ async function start() {
488
490
  // login() rejects on a bad token / network failure — catch it so a Discord
489
491
  // problem never propagates into the HTTP boot path.
490
492
  await c.login(token).catch((err) => {
491
- console.error('[discord-bot] login failed:', err.message);
493
+ log.error('[discord-bot] login failed:', err.message);
492
494
  // Tear the half-built client down so a later start() can retry cleanly.
493
495
  try { c.destroy(); } catch (_) { /* ignore */ }
494
496
  client = null;
495
497
  });
496
498
  return client;
497
499
  } catch (err) {
498
- console.error('[discord-bot] failed to start:', err.message);
500
+ log.error('[discord-bot] failed to start:', err.message);
499
501
  client = null;
500
502
  return null;
501
503
  } finally {
@@ -508,8 +510,8 @@ async function start() {
508
510
  function runRoleReconcile(trigger) {
509
511
  const roleSync = require('./discord-role-sync');
510
512
  roleSync.reconcileAll()
511
- .then((r) => console.log(`[discord-bot] role-sync reconcile (${trigger}): ${r.synced}/${r.total} linked builders synced.`))
512
- .catch((err) => console.error(`[discord-bot] role-sync reconcile (${trigger}) failed:`, err.message));
513
+ .then((r) => log.info(`[discord-bot] role-sync reconcile (${trigger}): ${r.synced}/${r.total} linked builders synced.`))
514
+ .catch((err) => log.error(`[discord-bot] role-sync reconcile (${trigger}) failed:`, err.message));
513
515
  }
514
516
 
515
517
  // Run the ADR 0037 channel reconcile sweep (best-effort, lazy-required). Makes
@@ -518,10 +520,10 @@ function runChannelReconcile(trigger) {
518
520
  const channels = require('./discord-channels');
519
521
  channels.reconcileChannels()
520
522
  .then((r) => {
521
- if (!r.ok) console.log(`[discord-bot] channel reconcile (${trigger}): skipped (${r.reason}).`);
522
- else console.log(`[discord-bot] channel reconcile (${trigger}): ${r.applied} applied, ${r.failed} failed of ${r.total}.`);
523
+ if (!r.ok) log.info(`[discord-bot] channel reconcile (${trigger}): skipped (${r.reason}).`);
524
+ else log.info(`[discord-bot] channel reconcile (${trigger}): ${r.applied} applied, ${r.failed} failed of ${r.total}.`);
523
525
  })
524
- .catch((err) => console.error(`[discord-bot] channel reconcile (${trigger}) failed:`, err.message));
526
+ .catch((err) => log.error(`[discord-bot] channel reconcile (${trigger}) failed:`, err.message));
525
527
  }
526
528
 
527
529
  // Graceful shutdown — close the gateway connection. Safe to call when never
@@ -536,9 +538,9 @@ async function stop() {
536
538
  client = null;
537
539
  try {
538
540
  await c.destroy();
539
- console.log('[discord-bot] disconnected.');
541
+ log.info('[discord-bot] disconnected.');
540
542
  } catch (err) {
541
- console.error('[discord-bot] error during shutdown:', err.message);
543
+ log.error('[discord-bot] error during shutdown:', err.message);
542
544
  }
543
545
  }
544
546
 
@@ -27,6 +27,8 @@ const path = require('node:path');
27
27
  const os = require('node:os');
28
28
  const db = require('./db');
29
29
  const ic = require('../../src/module-api');
30
+ // task 1003208: structured logging (pino via the doorway) — was console.*.
31
+ const log = ic.logger('discord');
30
32
 
31
33
  // The live ranks that map to a Discord role badge = the full rank ladder, in
32
34
  // order low→high. Sourced from db.LIVE_RANK_LADDER — the server-side single
@@ -134,7 +136,7 @@ async function syncBuilderRole(builderId, deps = realDeps) {
134
136
  return { ok: true, add, removed: remove };
135
137
  } catch (err) {
136
138
  // Most likely a Discord permission/hierarchy problem — log, don't throw.
137
- console.error(`[discord-bot] role-sync failed for builder ${builderId}:`, err.message);
139
+ log.error(`[discord-bot] role-sync failed for builder ${builderId}:`, err.message);
138
140
  return { ok: false, reason: 'error', error: err.message };
139
141
  }
140
142
  }
@@ -177,14 +179,14 @@ async function ensureRankRoles(guild) {
177
179
  });
178
180
  if (!res.ok) {
179
181
  const text = await res.text();
180
- console.error(`[discord-bot] ensureRankRoles: failed to create ${rank} role (${res.status}): ${text}`);
182
+ log.error(`[discord-bot] ensureRankRoles: failed to create ${rank} role (${res.status}): ${text}`);
181
183
  continue;
182
184
  }
183
185
  const role = await res.json();
184
186
  created[rank] = role.id;
185
- console.log(`[discord-bot] ensureRankRoles: created ${rank} role (id=${role.id})`);
187
+ log.info(`[discord-bot] ensureRankRoles: created ${rank} role (id=${role.id})`);
186
188
  } catch (e) {
187
- console.error(`[discord-bot] ensureRankRoles: error creating ${rank} role:`, e.message);
189
+ log.error(`[discord-bot] ensureRankRoles: error creating ${rank} role:`, e.message);
188
190
  }
189
191
  }
190
192
 
@@ -199,9 +201,9 @@ async function ensureRankRoles(guild) {
199
201
  const cfg = JSON.parse(ic.readConfigFileSync('discord-bot.json') || '{}');
200
202
  cfg.role_ids = Object.assign({}, cfg.role_ids || {}, created);
201
203
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 4), 'utf8');
202
- console.log(`[discord-bot] ensureRankRoles: saved ${Object.keys(created).join(', ')} to config.`);
204
+ log.info(`[discord-bot] ensureRankRoles: saved ${Object.keys(created).join(', ')} to config.`);
203
205
  } catch (e) {
204
- console.error('[discord-bot] ensureRankRoles: could not save role ids to config:', e.message);
206
+ log.error('[discord-bot] ensureRankRoles: could not save role ids to config:', e.message);
205
207
  }
206
208
  }
207
209
 
@@ -241,13 +243,13 @@ async function ensureRankRoles(guild) {
241
243
  body: JSON.stringify({ color: desired }),
242
244
  });
243
245
  if (recolorRes.ok) {
244
- console.log(`[discord-bot] ensureRankRoles: recolored ${rank} role to #${desired.toString(16).padStart(6, '0')}`);
246
+ log.info(`[discord-bot] ensureRankRoles: recolored ${rank} role to #${desired.toString(16).padStart(6, '0')}`);
245
247
  } else {
246
248
  const text = await recolorRes.text();
247
- console.error(`[discord-bot] ensureRankRoles: recolor ${rank} failed (${recolorRes.status}): ${text}`);
249
+ log.error(`[discord-bot] ensureRankRoles: recolor ${rank} failed (${recolorRes.status}): ${text}`);
248
250
  }
249
251
  } catch (e) {
250
- console.error(`[discord-bot] ensureRankRoles: error recoloring ${rank} role:`, e.message);
252
+ log.error(`[discord-bot] ensureRankRoles: error recoloring ${rank} role:`, e.message);
251
253
  }
252
254
  }
253
255
 
@@ -273,15 +275,15 @@ async function ensureRankRoles(guild) {
273
275
  });
274
276
  if (patchRes.ok) {
275
277
  const order = highToLow.filter((r) => finalMap[r]).join(' > ');
276
- console.log(`[discord-bot] ensureRankRoles: reordered roles (${order})`);
278
+ log.info(`[discord-bot] ensureRankRoles: reordered roles (${order})`);
277
279
  } else {
278
280
  const text = await patchRes.text();
279
- console.error(`[discord-bot] ensureRankRoles: role reorder failed (${patchRes.status}): ${text}`);
281
+ log.error(`[discord-bot] ensureRankRoles: role reorder failed (${patchRes.status}): ${text}`);
280
282
  }
281
283
  }
282
284
  }
283
285
  } catch (e) {
284
- console.error('[discord-bot] ensureRankRoles: error reordering roles:', e.message);
286
+ log.error('[discord-bot] ensureRankRoles: error reordering roles:', e.message);
285
287
  }
286
288
  }
287
289
 
@@ -25,6 +25,8 @@ const db = require('../db');
25
25
  const sessionScopes = require('../session-scopes');
26
26
  const boardMembership = require('../board-membership');
27
27
  const board = require('../board');
28
+ // task 1003208: structured logging (pino via the doorway) — was console.*.
29
+ const log = api.logger('government');
28
30
 
29
31
  // The port surface. Deliberately narrow: resolution + the catalog reads a gate or
30
32
  // the Archon tab needs. It grows with R98-R101 (principals, session-scopes,
@@ -208,7 +210,7 @@ module.exports = function buildGovernmentRouter() {
208
210
  const ranks = await db.listRanksWithGrants();
209
211
  res.json({ ranks });
210
212
  } catch (err) {
211
- console.error('[gds] GET /government/ranks', err);
213
+ log.error('[gds] GET /government/ranks', err);
212
214
  res.fail('ranks_list_failed', { status: 500, message: 'internal error' });
213
215
  }
214
216
  });
@@ -242,7 +244,7 @@ module.exports = function buildGovernmentRouter() {
242
244
  const assignments = await db.listAssignments();
243
245
  res.json({ assignments });
244
246
  } catch (err) {
245
- console.error('[gds] GET /government/assignments', err);
247
+ log.error('[gds] GET /government/assignments', err);
246
248
  res.fail('assignments_list_failed', { status: 500, message: 'internal error' });
247
249
  }
248
250
  });
@@ -272,7 +274,7 @@ module.exports = function buildGovernmentRouter() {
272
274
  if (err && err.code === '23505') {
273
275
  return res.fail('rank_exists', 409, { rank_key: rankKey });
274
276
  }
275
- console.error('[gds] POST /government/ranks', err);
277
+ log.error('[gds] POST /government/ranks', err);
276
278
  res.fail('rank_create_failed', { status: 500, message: 'internal error' });
277
279
  }
278
280
  });
@@ -307,7 +309,7 @@ module.exports = function buildGovernmentRouter() {
307
309
  const granted = await db.grantRankPermission({ rankKey, permissionKey });
308
310
  res.json({ ok: true, rank_key: rankKey, permission_key: permissionKey, granted });
309
311
  } catch (err) {
310
- console.error('[gds] PUT /government/ranks/:rankKey/permissions/:permissionKey', err);
312
+ log.error('[gds] PUT /government/ranks/:rankKey/permissions/:permissionKey', err);
311
313
  res.fail('grant_failed', { status: 500, message: 'internal error' });
312
314
  }
313
315
  });
@@ -326,7 +328,7 @@ module.exports = function buildGovernmentRouter() {
326
328
  const removed = await db.revokeRankPermission({ rankKey, permissionKey });
327
329
  res.json({ ok: true, rank_key: rankKey, permission_key: permissionKey, removed });
328
330
  } catch (err) {
329
- console.error('[gds] DELETE /government/ranks/:rankKey/permissions/:permissionKey', err);
331
+ log.error('[gds] DELETE /government/ranks/:rankKey/permissions/:permissionKey', err);
330
332
  res.fail('revoke_failed', { status: 500, message: 'internal error' });
331
333
  }
332
334
  });
@@ -361,7 +363,7 @@ module.exports = function buildGovernmentRouter() {
361
363
  const count = await db.resetRankGrants({ rankKey, permissionKeys: keys });
362
364
  res.json({ ok: true, rank_key: rankKey, seeded_rank: rank.seeded_rank, permission_count: count });
363
365
  } catch (err) {
364
- console.error('[gds] POST /government/ranks/:rankKey/reset', err);
366
+ log.error('[gds] POST /government/ranks/:rankKey/reset', err);
365
367
  res.fail('reset_failed', { status: 500, message: 'internal error' });
366
368
  }
367
369
  });
@@ -391,7 +393,7 @@ module.exports = function buildGovernmentRouter() {
391
393
  if (err && err.code === '23503') {
392
394
  return res.fail('builder_not_found', 404, { builder_id: req.body.builder_id });
393
395
  }
394
- console.error('[gds] POST /government/assignments', err);
396
+ log.error('[gds] POST /government/assignments', err);
395
397
  res.fail('assign_failed', { status: 500, message: 'internal error' });
396
398
  }
397
399
  });
@@ -409,7 +411,7 @@ module.exports = function buildGovernmentRouter() {
409
411
  const removed = await db.unassignRank({ builderId, rankKey });
410
412
  res.json({ ok: true, builder_id: builderId, rank_key: rankKey, removed });
411
413
  } catch (err) {
412
- console.error('[gds] DELETE /government/assignments/:builderId/:rankKey', err);
414
+ log.error('[gds] DELETE /government/assignments/:builderId/:rankKey', err);
413
415
  res.fail('unassign_failed', { status: 500, message: 'internal error' });
414
416
  }
415
417
  });
@@ -427,7 +429,7 @@ module.exports = function buildGovernmentRouter() {
427
429
  const view = await board.boardRoomView({ closedLimit: 20 });
428
430
  res.json({ ...view, viewer_id: String(req.builder.id) });
429
431
  } catch (err) {
430
- console.error('[gds] GET /government/board/items', err);
432
+ log.error('[gds] GET /government/board/items', err);
431
433
  res.fail('board_read_failed', { status: 500, message: 'internal error' });
432
434
  }
433
435
  });
@@ -442,7 +444,7 @@ module.exports = function buildGovernmentRouter() {
442
444
  try {
443
445
  res.json(await board.constitutionView({ historyLimit: 50 }));
444
446
  } catch (err) {
445
- console.error('[gds] GET /government/constitution', err);
447
+ log.error('[gds] GET /government/constitution', err);
446
448
  res.fail('constitution_read_failed', { status: 500, message: 'internal error' });
447
449
  }
448
450
  });
@@ -498,7 +500,7 @@ module.exports = function buildGovernmentRouter() {
498
500
  }
499
501
  res.status(201).json({ ok: true, amendment: out.amendment, item: out.item });
500
502
  } catch (err) {
501
- console.error('[gds] POST /government/board/items', err);
503
+ log.error('[gds] POST /government/board/items', err);
502
504
  res.fail('propose_failed', { status: 500, message: 'internal error' });
503
505
  }
504
506
  });
@@ -548,7 +550,7 @@ module.exports = function buildGovernmentRouter() {
548
550
  try {
549
551
  closure = await board.maybeCloseAfterVote(itemId);
550
552
  } catch (closeErr) {
551
- console.error('[gds] post-vote close failed (vote recorded, close left to the expiry sweep):', closeErr);
553
+ log.error('[gds] post-vote close failed (vote recorded, close left to the expiry sweep):', closeErr);
552
554
  }
553
555
  }
554
556
  // Idempotent re-cast reads back the standing vote — 200, not 201, and no
@@ -561,7 +563,7 @@ module.exports = function buildGovernmentRouter() {
561
563
  ...(closure && closure.closed ? { item_closed: true, outcome: closure.outcome } : {}),
562
564
  });
563
565
  } catch (err) {
564
- console.error('[gds] POST /government/board/items/:itemId/votes', err);
566
+ log.error('[gds] POST /government/board/items/:itemId/votes', err);
565
567
  res.fail('vote_failed', { status: 500, message: 'internal error' });
566
568
  }
567
569
  });
@@ -8,6 +8,8 @@ const inbox = require('../inbox');
8
8
  // and the deterministic scorer (R142) — both pure, both walk the declaration.
9
9
  const { validateFieldValues, templateFor, fieldOrder } = require('../templates');
10
10
  const { completenessScore, weakestSectionPayload, scoreFieldValues } = require('../completeness');
11
+ // task 1003208: structured logging (pino via the doorway) — was console.*.
12
+ const log = api.logger('ideas');
11
13
  const { validateOrRespond, LIMITS, parseId } = api;
12
14
 
13
15
  const INBOX_STATUS_VALUES = ['open', 'promoted', 'discarded', 'merged'];
@@ -94,7 +96,7 @@ module.exports = function buildInboxRouter() {
94
96
  try {
95
97
  v = validateFieldValues(body.field_values ?? {}, templateKey ? { templateKey } : undefined);
96
98
  } catch (validationErr) {
97
- console.error('[gds] POST /inbox field-values validation threw:', validationErr);
99
+ log.error('[gds] POST /inbox field-values validation threw:', validationErr);
98
100
  return res.fail('invalid_field_values', 400, { errors: [{ field: null, reason: 'unvalidatable' }], template_key: templateKey ?? null });
99
101
  }
100
102
  if (!v.ok) return res.fail('invalid_field_values', 400, { errors: v.errors, template_key: templateKey ?? null });
@@ -155,7 +157,7 @@ module.exports = function buildInboxRouter() {
155
157
  if (r && r.credited > 0) idea.ideator_credit = r.credited;
156
158
  }
157
159
  } catch (creditErr) {
158
- console.error('[gds] POST /inbox full-idea pass-credit failed (idea captured + graded, credit skipped):', creditErr);
160
+ log.error('[gds] POST /inbox full-idea pass-credit failed (idea captured + graded, credit skipped):', creditErr);
159
161
  // no warning surfaced — the idea and its grade are intact; the credit
160
162
  // is the only thing missed, and a reconcile/re-grade path can pay it.
161
163
  }
@@ -193,12 +195,12 @@ module.exports = function buildInboxRouter() {
193
195
  }
194
196
  }
195
197
  } catch (boardErr) {
196
- console.error('[gds] POST /inbox board-window open failed (idea captured + graded, window skipped):', boardErr);
198
+ log.error('[gds] POST /inbox board-window open failed (idea captured + graded, window skipped):', boardErr);
197
199
  // R11's expiry poller sweeps open items, not missed opens; a missed
198
200
  // open is recoverable by resubmission (R13) — capture stays intact.
199
201
  }
200
202
  } catch (gradeErr) {
201
- console.error('[gds] POST /inbox full-grade write failed (idea captured, landing as quick):', gradeErr);
203
+ log.error('[gds] POST /inbox full-grade write failed (idea captured, landing as quick):', gradeErr);
202
204
  grade_warning = 'captured_but_ungraded';
203
205
  }
204
206
  }
@@ -242,7 +244,7 @@ module.exports = function buildInboxRouter() {
242
244
  500;
243
245
  return res.fail(err.code, status, { ...err });
244
246
  }
245
- console.error('[gds] POST /inbox', err);
247
+ log.error('[gds] POST /inbox', err);
246
248
  res.fail('capture_failed', { status: 500, message: 'internal error' });
247
249
  }
248
250
  });
@@ -272,7 +274,7 @@ module.exports = function buildInboxRouter() {
272
274
  const open_count = await inbox.countOpenIdeas({ viewerRank });
273
275
  res.json({ ideas, open_count });
274
276
  } catch (err) {
275
- console.error('[gds] GET /inbox', err);
277
+ log.error('[gds] GET /inbox', err);
276
278
  res.fail('list_failed', { status: 500, message: 'internal error' });
277
279
  }
278
280
  });
@@ -451,7 +453,7 @@ module.exports = function buildInboxRouter() {
451
453
  if (!idea) return res.fail('idea_not_found', 404);
452
454
  res.json({ idea });
453
455
  } catch (err) {
454
- console.error('[gds] GET /inbox/:id', err);
456
+ log.error('[gds] GET /inbox/:id', err);
455
457
  res.fail('idea_read_failed', { status: 500, message: 'internal error' });
456
458
  }
457
459
  });
@@ -494,7 +496,7 @@ module.exports = function buildInboxRouter() {
494
496
  try {
495
497
  v = validateFieldValues(req.body.field_values, templateKey ? { templateKey } : undefined);
496
498
  } catch (validationErr) {
497
- console.error('[gds] POST /inbox/:id/resubmit validation threw:', validationErr);
499
+ log.error('[gds] POST /inbox/:id/resubmit validation threw:', validationErr);
498
500
  return res.fail('invalid_field_values', 400, { errors: [{ field: null, reason: 'unvalidatable' }] });
499
501
  }
500
502
  if (!v.ok) return res.fail('invalid_field_values', 400, { errors: v.errors, template_key: templateKey ?? null });
@@ -507,7 +509,7 @@ module.exports = function buildInboxRouter() {
507
509
  let rejection = null;
508
510
  if (government && government.latestRejectionForIdea) {
509
511
  try { rejection = await government.latestRejectionForIdea(id); }
510
- catch (e) { console.error('[gds] resubmit: rejection lookup failed (continuing without):', e); }
512
+ catch (e) { log.error('[gds] resubmit: rejection lookup failed (continuing without):', e); }
511
513
  }
512
514
  if (!rejection || !rejection.section_key) {
513
515
  const flag = weakestSectionPayload(idea.field_values, templateKey ? { templateKey } : undefined);
@@ -539,7 +541,7 @@ module.exports = function buildInboxRouter() {
539
541
  if (r && r.credited > 0) out.ideator_credit = r.credited;
540
542
  }
541
543
  } catch (creditErr) {
542
- console.error('[gds] resubmit: pass-credit failed (regrade intact, credit skipped):', creditErr);
544
+ log.error('[gds] resubmit: pass-credit failed (regrade intact, credit skipped):', creditErr);
543
545
  }
544
546
 
545
547
  let board_window;
@@ -553,7 +555,7 @@ module.exports = function buildInboxRouter() {
553
555
  }
554
556
  }
555
557
  } catch (boardErr) {
556
- console.error('[gds] resubmit: board-window open failed (regrade intact, window skipped):', boardErr);
558
+ log.error('[gds] resubmit: board-window open failed (regrade intact, window skipped):', boardErr);
557
559
  }
558
560
 
559
561
  res.json({
@@ -563,7 +565,7 @@ module.exports = function buildInboxRouter() {
563
565
  ...(board_window ? { board_window } : {}),
564
566
  });
565
567
  } catch (err) {
566
- console.error('[gds] POST /inbox/:id/resubmit', err);
568
+ log.error('[gds] POST /inbox/:id/resubmit', err);
567
569
  res.fail('resubmit_failed', { status: 500, message: 'internal error' });
568
570
  }
569
571
  });
@@ -994,7 +996,7 @@ module.exports = function buildInboxRouter() {
994
996
  500;
995
997
  return res.fail(err.code, status, { ...err });
996
998
  }
997
- console.error('[gds] PATCH /inbox/:id', err);
999
+ log.error('[gds] PATCH /inbox/:id', err);
998
1000
  res.fail('update_failed', { status: 500, message: 'internal error' });
999
1001
  }
1000
1002
  });
@@ -1050,7 +1052,7 @@ module.exports = function buildInboxRouter() {
1050
1052
  if (err && err.code === '23505') {
1051
1053
  return res.fail('CRITERION_ALREADY_RATIFIED', 409, { detail: 'A criterion with this proposal ref already exists on the target version.' });
1052
1054
  }
1053
- console.error('[gds] POST /inbox/:id/ratify', err);
1055
+ log.error('[gds] POST /inbox/:id/ratify', err);
1054
1056
  res.fail('ratify_failed', { status: 500, message: 'internal error' });
1055
1057
  }
1056
1058
  }
@@ -12,6 +12,8 @@ const { validateOrRespond, LIMITS, parseId } = api;
12
12
  // task 1003153: the DEAD dependency edge (a dep that was abandoned, so can never
13
13
  // satisfy) — one pure definition shared with the CLI + the detector.
14
14
  const { deadDepHint, splitDeadDeps } = require('../dead-deps.js');
15
+ // task 1003208: structured logging (pino via the doorway) — was console.*.
16
+ const log = api.logger('lifecycle');
15
17
 
16
18
  // V3.R33 (#254): input validation guards. Length caps + shape checks at the
17
19
  // top of every write handler. Schemas describe KNOWN-field constraints only —
@@ -155,7 +157,7 @@ async function gateOneActiveClaimPerSession(req, res, next) {
155
157
  activeClaimsForSession = await db.getActiveClaimsForSession(creatorSessionId);
156
158
  } catch (err) {
157
159
  // FAIL-CLOSED. A lookup failure must not let a commingling second claim land.
158
- console.error('[gds] gateOneActiveClaimPerSession lookup failed (failing closed):', err);
160
+ log.error('[gds] gateOneActiveClaimPerSession lookup failed (failing closed):', err);
159
161
  return res.fail('session_claim_check_failed', { status: 409, message: 'Could not verify this worktree has no other claim in flight. Try again, or start a fresh worktree for this task.' });
160
162
  }
161
163
  const decision = shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId, worktreeName });
@@ -188,7 +190,7 @@ async function gateNewcomerClaim(req, res, next) {
188
190
  }
189
191
  next();
190
192
  } catch (err) {
191
- console.error('[gds] gateNewcomerClaim failed:', err);
193
+ log.error('[gds] gateNewcomerClaim failed:', err);
192
194
  // Fail open — onboarding plumbing must never brick a claim attempt.
193
195
  next();
194
196
  }
@@ -249,7 +251,7 @@ module.exports = function buildClaimsRouter() {
249
251
  try {
250
252
  await onboarding.restockForClaim(task);
251
253
  } catch (e) {
252
- console.log('[gds] newcomer chore restock after claim failed (non-blocking):', e && e.message);
254
+ log.info('[gds] newcomer chore restock after claim failed (non-blocking):', e && e.message);
253
255
  }
254
256
  }
255
257
  // V3.R51 (#270): claim.worktree_name is the SERVER-DECIDED name. If the
@@ -344,7 +346,7 @@ module.exports = function buildClaimsRouter() {
344
346
  }
345
347
  return res.fail(err.code, status, { ...err });
346
348
  }
347
- console.error('[gds] POST /claims', err);
349
+ log.error('[gds] POST /claims', err);
348
350
  res.fail('claim_failed', { status: 500, message: 'internal error' });
349
351
  }
350
352
  });
@@ -407,7 +409,7 @@ module.exports = function buildClaimsRouter() {
407
409
  if (err && err.code === 'BUILDER_INACTIVE') {
408
410
  return res.fail('BUILDER_INACTIVE', 403);
409
411
  }
410
- console.error('[gds] POST /claims/batch', err);
412
+ log.error('[gds] POST /claims/batch', err);
411
413
  res.fail('batch_claim_failed', { status: 500, message: 'internal error' });
412
414
  }
413
415
  });
@@ -445,7 +447,7 @@ module.exports = function buildClaimsRouter() {
445
447
  if (err && err.code === 'BATCH_EMPTY') {
446
448
  return res.fail('claim_ids_required', 400);
447
449
  }
448
- console.error('[gds] POST /claims/batch/release', err);
450
+ log.error('[gds] POST /claims/batch/release', err);
449
451
  res.fail('batch_release_failed', { status: 500, message: 'internal error' });
450
452
  }
451
453
  });
@@ -479,7 +481,7 @@ module.exports = function buildClaimsRouter() {
479
481
  if (err && err.code === 'BATCH_EMPTY') {
480
482
  return res.fail('task_ids_required', 400);
481
483
  }
482
- console.error('[gds] POST /claims/batch/validate', err);
484
+ log.error('[gds] POST /claims/batch/validate', err);
483
485
  res.fail('batch_validate_failed', { status: 500, message: 'internal error' });
484
486
  }
485
487
  });
@@ -543,7 +545,7 @@ module.exports = function buildClaimsRouter() {
543
545
  500;
544
546
  return res.fail(err.code, status, { ...err });
545
547
  }
546
- console.error('[gds] POST /claims/:id/resolve', err);
548
+ log.error('[gds] POST /claims/:id/resolve', err);
547
549
  res.fail('resolve_failed', { status: 500, message: 'internal error' });
548
550
  }
549
551
  });
@@ -596,7 +598,7 @@ module.exports = function buildClaimsRouter() {
596
598
  if (err && err.code === 'BAD_CLAIM_ID') {
597
599
  return res.fail('bad_claim_id', 400);
598
600
  }
599
- console.error('[gds] POST /claims/:id/release-on-behalf', err);
601
+ log.error('[gds] POST /claims/:id/release-on-behalf', err);
600
602
  res.fail('release_on_behalf_failed', { status: 500, message: 'internal error' });
601
603
  }
602
604
  }