@jungjaehoon/mama-os 0.22.0 → 0.23.0

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.
Files changed (128) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -1
  3. package/dist/agent/agent-loop.d.ts +0 -3
  4. package/dist/agent/agent-loop.d.ts.map +1 -1
  5. package/dist/agent/agent-loop.js +49 -34
  6. package/dist/agent/agent-loop.js.map +1 -1
  7. package/dist/agent/gateway-tool-executor.d.ts +18 -0
  8. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  9. package/dist/agent/gateway-tool-executor.js +185 -6
  10. package/dist/agent/gateway-tool-executor.js.map +1 -1
  11. package/dist/agent/gateway-tools.md +10 -5
  12. package/dist/agent/role-manager.d.ts +22 -1
  13. package/dist/agent/role-manager.d.ts.map +1 -1
  14. package/dist/agent/role-manager.js +39 -2
  15. package/dist/agent/role-manager.js.map +1 -1
  16. package/dist/agent/tool-registry.d.ts.map +1 -1
  17. package/dist/agent/tool-registry.js +35 -5
  18. package/dist/agent/tool-registry.js.map +1 -1
  19. package/dist/agent/types.d.ts +9 -1
  20. package/dist/agent/types.d.ts.map +1 -1
  21. package/dist/agent/types.js.map +1 -1
  22. package/dist/api/graph-api.d.ts.map +1 -1
  23. package/dist/api/graph-api.js +43 -33
  24. package/dist/api/graph-api.js.map +1 -1
  25. package/dist/api/operator-tasks-handler.d.ts.map +1 -1
  26. package/dist/api/operator-tasks-handler.js +6 -2
  27. package/dist/api/operator-tasks-handler.js.map +1 -1
  28. package/dist/cli/commands/start.d.ts.map +1 -1
  29. package/dist/cli/commands/start.js +185 -2
  30. package/dist/cli/commands/start.js.map +1 -1
  31. package/dist/cli/config/config-manager.d.ts +0 -5
  32. package/dist/cli/config/config-manager.d.ts.map +1 -1
  33. package/dist/cli/config/config-manager.js +57 -2
  34. package/dist/cli/config/config-manager.js.map +1 -1
  35. package/dist/cli/config/types.d.ts.map +1 -1
  36. package/dist/cli/config/types.js +35 -0
  37. package/dist/cli/config/types.js.map +1 -1
  38. package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
  39. package/dist/cli/runtime/agent-loop-init.js +96 -51
  40. package/dist/cli/runtime/agent-loop-init.js.map +1 -1
  41. package/dist/cli/runtime/api-routes-init.d.ts +3 -0
  42. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  43. package/dist/cli/runtime/api-routes-init.js +293 -196
  44. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  45. package/dist/cli/runtime/gateway-init.d.ts.map +1 -1
  46. package/dist/cli/runtime/gateway-init.js +5 -0
  47. package/dist/cli/runtime/gateway-init.js.map +1 -1
  48. package/dist/cli/runtime/gateway-wiring.d.ts.map +1 -1
  49. package/dist/cli/runtime/gateway-wiring.js +6 -0
  50. package/dist/cli/runtime/gateway-wiring.js.map +1 -1
  51. package/dist/envelope/reactive-config.d.ts.map +1 -1
  52. package/dist/envelope/reactive-config.js +26 -1
  53. package/dist/envelope/reactive-config.js.map +1 -1
  54. package/dist/gateways/message-router.d.ts +18 -1
  55. package/dist/gateways/message-router.d.ts.map +1 -1
  56. package/dist/gateways/message-router.js +155 -22
  57. package/dist/gateways/message-router.js.map +1 -1
  58. package/dist/gateways/telegram.d.ts +1 -0
  59. package/dist/gateways/telegram.d.ts.map +1 -1
  60. package/dist/gateways/telegram.js +38 -1
  61. package/dist/gateways/telegram.js.map +1 -1
  62. package/dist/gateways/types.d.ts +3 -0
  63. package/dist/gateways/types.d.ts.map +1 -1
  64. package/dist/memory/history-extractor.d.ts +0 -24
  65. package/dist/memory/history-extractor.d.ts.map +1 -1
  66. package/dist/memory/history-extractor.js +0 -127
  67. package/dist/memory/history-extractor.js.map +1 -1
  68. package/dist/memory/save-candidate-extractor.d.ts +3 -0
  69. package/dist/memory/save-candidate-extractor.d.ts.map +1 -1
  70. package/dist/memory/save-candidate-extractor.js +12 -1
  71. package/dist/memory/save-candidate-extractor.js.map +1 -1
  72. package/dist/memory/secret-filter.d.ts +22 -0
  73. package/dist/memory/secret-filter.d.ts.map +1 -0
  74. package/dist/memory/secret-filter.js +77 -0
  75. package/dist/memory/secret-filter.js.map +1 -0
  76. package/dist/observability/code-audit.d.ts +87 -0
  77. package/dist/observability/code-audit.d.ts.map +1 -0
  78. package/dist/observability/code-audit.js +403 -0
  79. package/dist/observability/code-audit.js.map +1 -0
  80. package/dist/operator/briefs.d.ts +26 -0
  81. package/dist/operator/briefs.d.ts.map +1 -0
  82. package/dist/operator/briefs.js +128 -0
  83. package/dist/operator/briefs.js.map +1 -0
  84. package/dist/operator/operator-trigger-loop.d.ts +24 -0
  85. package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
  86. package/dist/operator/operator-trigger-loop.js +71 -12
  87. package/dist/operator/operator-trigger-loop.js.map +1 -1
  88. package/dist/operator/report-carry.d.ts +29 -0
  89. package/dist/operator/report-carry.d.ts.map +1 -0
  90. package/dist/operator/report-carry.js +81 -0
  91. package/dist/operator/report-carry.js.map +1 -0
  92. package/dist/operator/shadow-capture.d.ts +20 -0
  93. package/dist/operator/shadow-capture.d.ts.map +1 -0
  94. package/dist/operator/shadow-capture.js +34 -0
  95. package/dist/operator/shadow-capture.js.map +1 -0
  96. package/dist/operator/situation-report.d.ts +6 -0
  97. package/dist/operator/situation-report.d.ts.map +1 -1
  98. package/dist/operator/situation-report.js +15 -1
  99. package/dist/operator/situation-report.js.map +1 -1
  100. package/dist/operator/task-ledger.d.ts +90 -2
  101. package/dist/operator/task-ledger.d.ts.map +1 -1
  102. package/dist/operator/task-ledger.js +359 -23
  103. package/dist/operator/task-ledger.js.map +1 -1
  104. package/dist/operator/worker-run.d.ts +59 -0
  105. package/dist/operator/worker-run.d.ts.map +1 -0
  106. package/dist/operator/worker-run.js +64 -0
  107. package/dist/operator/worker-run.js.map +1 -0
  108. package/dist/operator/workorder-consumer.d.ts +118 -0
  109. package/dist/operator/workorder-consumer.d.ts.map +1 -0
  110. package/dist/operator/workorder-consumer.js +248 -0
  111. package/dist/operator/workorder-consumer.js.map +1 -0
  112. package/dist/operator/workorder-hooks.d.ts +30 -0
  113. package/dist/operator/workorder-hooks.d.ts.map +1 -0
  114. package/dist/operator/workorder-hooks.js +70 -0
  115. package/dist/operator/workorder-hooks.js.map +1 -0
  116. package/dist/operator/workorder-publishers.d.ts +68 -0
  117. package/dist/operator/workorder-publishers.d.ts.map +1 -0
  118. package/dist/operator/workorder-publishers.js +150 -0
  119. package/dist/operator/workorder-publishers.js.map +1 -0
  120. package/dist/security/security-monitor.d.ts +19 -0
  121. package/dist/security/security-monitor.d.ts.map +1 -1
  122. package/dist/security/security-monitor.js +68 -14
  123. package/dist/security/security-monitor.js.map +1 -1
  124. package/dist/utils/untrusted-content.d.ts +31 -0
  125. package/dist/utils/untrusted-content.d.ts.map +1 -0
  126. package/dist/utils/untrusted-content.js +77 -0
  127. package/dist/utils/untrusted-content.js.map +1 -0
  128. package/package.json +2 -2
@@ -56,11 +56,30 @@ const upload_handler_js_1 = require("../../api/upload-handler.js");
56
56
  const operator_tasks_handler_js_1 = require("../../api/operator-tasks-handler.js");
57
57
  const auth_middleware_js_1 = require("../../api/auth-middleware.js");
58
58
  const utilities_js_1 = require("./utilities.js");
59
+ const code_audit_js_1 = require("../../observability/code-audit.js");
60
+ const workorder_publishers_js_1 = require("../../operator/workorder-publishers.js");
61
+ const security_monitor_js_1 = require("../../security/security-monitor.js");
59
62
  const debugLogger = __importStar(require("@jungjaehoon/mama-core/debug-logger"));
60
63
  const { DebugLogger } = debugLogger;
61
64
  const routesLogger = new DebugLogger('api-routes');
62
65
  async function registerApiRoutes(params) {
63
- const { config, apiServer, eventBus, oauthManager: _oauthManager, mamaApi: _mamaApi, messageRouter, agentLoop, toolExecutor, discordGateway, slackGateway, graphHandler, getAdapter, sessionsDb, } = params;
66
+ const { config, apiServer, eventBus, oauthManager: _oauthManager, mamaApi: _mamaApi, messageRouter, agentLoop, toolExecutor, discordGateway, slackGateway, graphHandler, getAdapter, sessionsDb, workOrderConsumer, } = params;
67
+ // ── Stage-2 publisher gate (plan S2-T2) ───────────────────────────────
68
+ // Throws at boot on a malformed flag value (no-fallback: a typo must not
69
+ // silently revert a believed-active migration to legacy).
70
+ const stage2Flag = (0, workorder_publishers_js_1.readStage2Flag)();
71
+ if (stage2Flag !== 'off') {
72
+ console.log(`[stage2] workorder publishers active (flag=${stage2Flag})`);
73
+ }
74
+ const enqueueWorkOrderOrThrow = (workKind, idempotencyKey, payload, priority) => {
75
+ const ledger = toolExecutor.getTaskLedger();
76
+ if (!ledger) {
77
+ throw new Error(`[stage2] TaskLedger unavailable - cannot enqueue ${workKind} workorder`);
78
+ }
79
+ (0, workorder_publishers_js_1.validateWorkOrderPayload)(workKind, payload);
80
+ const wo = ledger.enqueueWorkOrder({ workKind, idempotencyKey, input: payload, priority });
81
+ console.log(`[stage2] enqueued workorder ${workKind}#${wo.id} key=${idempotencyKey}`);
82
+ };
64
83
  // ── Validation Session Service ────────────────────────────────────────
65
84
  const validationService = sessionsDb ? new session_service_js_1.ValidationSessionService(sessionsDb) : null;
66
85
  /**
@@ -259,6 +278,22 @@ async function registerApiRoutes(params) {
259
278
  {
260
279
  const { createReportPublisher } = await import('../../api/report-handler.js');
261
280
  toolExecutor.setReportPublisher(createReportPublisher(apiServer.reportStore, apiServer.reportSseClients));
281
+ // S1-T4 artifact hub: the console READS the board it directs. Same store
282
+ // the publisher writes - the conductor's status answers cite these slots
283
+ // instead of re-deriving state from memory copies.
284
+ toolExecutor.setReportReader(() => {
285
+ const slots = apiServer.reportStore.getAll();
286
+ const projected = {};
287
+ for (const [name, slot] of Object.entries(slots)) {
288
+ projected[name] = {
289
+ html: slot.html,
290
+ updatedAt: Number.isFinite(slot.updatedAt)
291
+ ? new Date(slot.updatedAt).toISOString()
292
+ : null,
293
+ };
294
+ }
295
+ return projected;
296
+ });
262
297
  }
263
298
  if (dashboardAgentConfigured) {
264
299
  // ── Dashboard Agent ───────────────────────────────────────────────
@@ -284,6 +319,23 @@ This saves resources. Only publish when there is genuinely new information to re
284
319
  // request means "rebuild the board now", not "tell me nothing changed".
285
320
  const buildForcedDashboardPrompt = () => buildDashboardPrompt().replace(/3\. If NO substantive decisions[^\n]*\n/, '3. The owner explicitly requested a fresh board: do NOT reply NO_UPDATE. Rebuild and publish even if nothing changed since the last publish.\n');
286
321
  const doDashboardRun = async (opts) => {
322
+ // Stage-2 gate at the FUNCTION level: schedule, boot, manual REST all
323
+ // funnel here, so this single gate covers every entry path (plan F4).
324
+ const boardAction = (0, workorder_publishers_js_1.resolvePublishAction)(stage2Flag, 'board');
325
+ if (boardAction !== 'legacy') {
326
+ try {
327
+ const now = Date.now();
328
+ enqueueWorkOrderOrThrow('board', opts?.force ? (0, workorder_publishers_js_1.boardManualKey)(now) : (0, workorder_publishers_js_1.boardFullKey)(now), { mode: 'full', ...(opts?.force ? { force: true } : {}) }, opts?.force ? 'high' : undefined);
329
+ }
330
+ catch (err) {
331
+ routesLogger.error('[stage2] board enqueue failed:', err instanceof Error ? err.message : err);
332
+ }
333
+ // 'enqueue' (on): legacy stops - an enqueue failure must NOT silently
334
+ // fall back to a legacy run; the next occurrence retries.
335
+ // 'both' (shadow): legacy keeps publishing live below.
336
+ if (boardAction === 'enqueue')
337
+ return;
338
+ }
287
339
  const pm = toolExecutor.getAgentProcessManager();
288
340
  if (!pm) {
289
341
  routesLogger.warn('[Dashboard Agent] AgentProcessManager not available yet');
@@ -362,52 +414,130 @@ This saves resources. Only publish when there is genuinely new information to re
362
414
  return row.n;
363
415
  },
364
416
  };
417
+ // Stage-2 board completion hook (plan D1/E3/G1): the consumer wraps
418
+ // reconcile workorders with the SAME bracket the legacy closure owns,
419
+ // but trace rows from worker runs carry the worker's identity, not
420
+ // agent_id='dashboard-agent' - the re-key goes through details JSON
421
+ // (agent_activity has NO channel_id column; json_extract is the only
422
+ // schema-supported key; agent_id re-keying is a green-test trap).
423
+ if (workOrderConsumer) {
424
+ const { buildWorkerTraceQueries } = await import('../../operator/workorder-hooks.js');
425
+ const workerVerifierDeps = {
426
+ ...verifierDeps,
427
+ ...buildWorkerTraceQueries(sessionsDb, 'worker:board'),
428
+ };
429
+ workOrderConsumer.registerHook('board', {
430
+ before: (wo) => {
431
+ if (wo.payload.mode !== 'reconcile')
432
+ return null;
433
+ return captureSnapshot(workerVerifierDeps, `reconcile:${String(wo.payload.channelKey)}`);
434
+ },
435
+ after: (wo, response, beforeState) => {
436
+ if (wo.payload.mode !== 'reconcile' || !beforeState) {
437
+ // FULL runs are not bracket-verified (plan: reconcile mode only).
438
+ if (response.includes('NO_UPDATE')) {
439
+ console.log('[stage2] board worker: no changes detected, publish skipped');
440
+ }
441
+ return;
442
+ }
443
+ const scope = `reconcile:${String(wo.payload.channelKey)}`;
444
+ const verdict = verifyAfterRun(workerVerifierDeps, beforeState, scope);
445
+ const outcome = verdict.verified ? 'reconcile_verified' : 'reconcile_unverified';
446
+ console.log(`[stage2] ${outcome} channel=${String(wo.payload.channelKey)}${verdict.effects.length > 0 ? ` (${verdict.effects.join('; ')})` : ''}`);
447
+ try {
448
+ if (sessionsDb) {
449
+ (0, agent_store_js_1.logActivity)(sessionsDb, {
450
+ agent_id: 'workorder-board',
451
+ agent_version: 0,
452
+ type: outcome,
453
+ input_summary: `reconcile ${String(wo.payload.channelKey)}`,
454
+ output_summary: verdict.effects.join('; '),
455
+ execution_status: 'completed',
456
+ trigger_reason: 'workorder',
457
+ });
458
+ }
459
+ }
460
+ catch {
461
+ /* telemetry only */
462
+ }
463
+ if (!verdict.verified) {
464
+ eventBus.emit({
465
+ type: 'agent:action',
466
+ agent: 'Dashboard Agent',
467
+ action: 'reconcile_unverified',
468
+ target: String(wo.payload.channelKey),
469
+ });
470
+ }
471
+ },
472
+ });
473
+ }
365
474
  const reconcileScheduler = new ReconcileScheduler({
366
475
  debounceMs: Number(process.env.MAMA_RECONCILE_DEBOUNCE_MS) || undefined,
367
476
  maxWaitMs: Number(process.env.MAMA_RECONCILE_MAX_WAIT_MS) || undefined,
368
477
  globalMaxPerHour: Number(process.env.MAMA_RECONCILE_MAX_PER_HOUR) || undefined,
369
478
  log: (line) => console.log(line),
370
- run: (channelKey, deltaLines) => boardWriterQueue.push(async () => {
371
- const scope = `reconcile:${channelKey}`;
372
- const before = captureSnapshot(verifierDeps, scope);
373
- const prompt = buildReconcilePrompt({
374
- channelKey,
375
- deltaLines,
376
- todayIso: new Date().toISOString().slice(0, 10),
377
- kagemushaContext,
378
- });
379
- await executeValidatedRun('dashboard-agent', prompt, {
380
- requestTimeout: 300_000,
381
- });
382
- const verdict = verifyAfterRun(verifierDeps, before, scope);
383
- const outcome = verdict.verified ? 'reconcile_verified' : 'reconcile_unverified';
384
- console.log(`[reconcile] ${outcome} channel=${channelKey}${verdict.effects.length > 0 ? ` (${verdict.effects.join('; ')})` : ''}`);
385
- try {
386
- if (sessionsDb) {
387
- (0, agent_store_js_1.logActivity)(sessionsDb, {
388
- agent_id: 'dashboard-agent',
389
- agent_version: 0,
390
- type: outcome,
391
- input_summary: `reconcile ${channelKey}`,
392
- output_summary: verdict.effects.join('; '),
393
- execution_status: 'completed',
394
- trigger_reason: 'reconcile',
479
+ run: (channelKey, deltaLines) => {
480
+ // Stage-2: at 'on' the reconcile leg becomes a board workorder (its
481
+ // bracket verification moves to the consumer's completion hook);
482
+ // shadow/off keep the legacy bracket-verified path (plan C1).
483
+ if ((0, workorder_publishers_js_1.resolveReconcileAction)(stage2Flag) === 'enqueue') {
484
+ try {
485
+ enqueueWorkOrderOrThrow('board', (0, workorder_publishers_js_1.boardReconcileKey)(channelKey, Date.now()), {
486
+ mode: 'reconcile',
487
+ channelKey,
488
+ deltaLines,
395
489
  });
396
490
  }
491
+ catch (err) {
492
+ routesLogger.error('[stage2] reconcile enqueue failed:', err instanceof Error ? err.message : err);
493
+ // REJECT so ReconcileScheduler restores pendingLines - a
494
+ // resolved failure silently drops this delta (PR bot round).
495
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
496
+ }
497
+ return Promise.resolve();
397
498
  }
398
- catch {
399
- /* telemetry only */
400
- }
401
- if (!verdict.verified) {
402
- // Loud, never blocking (observability over restriction).
403
- eventBus.emit({
404
- type: 'agent:action',
405
- agent: 'Dashboard Agent',
406
- action: 'reconcile_unverified',
407
- target: channelKey,
499
+ return boardWriterQueue.push(async () => {
500
+ const scope = `reconcile:${channelKey}`;
501
+ const before = captureSnapshot(verifierDeps, scope);
502
+ const prompt = buildReconcilePrompt({
503
+ channelKey,
504
+ deltaLines,
505
+ todayIso: new Date().toISOString().slice(0, 10),
506
+ kagemushaContext,
408
507
  });
409
- }
410
- }),
508
+ await executeValidatedRun('dashboard-agent', prompt, {
509
+ requestTimeout: 300_000,
510
+ });
511
+ const verdict = verifyAfterRun(verifierDeps, before, scope);
512
+ const outcome = verdict.verified ? 'reconcile_verified' : 'reconcile_unverified';
513
+ console.log(`[reconcile] ${outcome} channel=${channelKey}${verdict.effects.length > 0 ? ` (${verdict.effects.join('; ')})` : ''}`);
514
+ try {
515
+ if (sessionsDb) {
516
+ (0, agent_store_js_1.logActivity)(sessionsDb, {
517
+ agent_id: 'dashboard-agent',
518
+ agent_version: 0,
519
+ type: outcome,
520
+ input_summary: `reconcile ${channelKey}`,
521
+ output_summary: verdict.effects.join('; '),
522
+ execution_status: 'completed',
523
+ trigger_reason: 'reconcile',
524
+ });
525
+ }
526
+ }
527
+ catch {
528
+ /* telemetry only */
529
+ }
530
+ if (!verdict.verified) {
531
+ // Loud, never blocking (observability over restriction).
532
+ eventBus.emit({
533
+ type: 'agent:action',
534
+ agent: 'Dashboard Agent',
535
+ action: 'reconcile_unverified',
536
+ target: channelKey,
537
+ });
538
+ }
539
+ });
540
+ },
411
541
  });
412
542
  eventBus.on('operator:channel-delta', (event) => {
413
543
  if (event.type === 'operator:channel-delta') {
@@ -482,7 +612,20 @@ This saves resources. Only publish when there is genuinely new information to re
482
612
  });
483
613
  });
484
614
  // Wiki trigger via executeValidatedRun
485
- const doWikiRun = async () => {
615
+ const doWikiRun = async (trigger) => {
616
+ // Stage-2 gate (function level, plan F4). Wiki never enters shadow
617
+ // (resolvePublishAction returns 'legacy' there) - its Obsidian writes
618
+ // have no capture seam.
619
+ if ((0, workorder_publishers_js_1.resolvePublishAction)(stage2Flag, 'wiki') === 'enqueue') {
620
+ try {
621
+ const now = Date.now();
622
+ enqueueWorkOrderOrThrow('wiki', (0, workorder_publishers_js_1.wikiBatchKey)(trigger, now), { batchId: `${now}-${trigger}`, events: [trigger] }, trigger === 'manual' ? 'high' : undefined);
623
+ }
624
+ catch (err) {
625
+ routesLogger.error('[stage2] wiki enqueue failed:', err instanceof Error ? err.message : err);
626
+ }
627
+ return;
628
+ }
486
629
  if (!toolExecutor.getAgentProcessManager()) {
487
630
  routesLogger.warn('[Wiki Agent] AgentProcessManager not available yet');
488
631
  return;
@@ -517,21 +660,29 @@ This saves resources. Only compile when there is genuinely new information to do
517
660
  routesLogger.error('[Wiki Agent] Error:', err instanceof Error ? err.message : err);
518
661
  }
519
662
  };
663
+ // Stage-2 wiki completion hook (plan E4): outcome reading only - the
664
+ // wiki:compiled events flow through the wikiPublisher independently.
665
+ if (workOrderConsumer) {
666
+ const { buildWikiAfterHook } = await import('../../operator/workorder-hooks.js');
667
+ workOrderConsumer.registerHook('wiki', {
668
+ after: buildWikiAfterHook((line) => routesLogger.debug(line)),
669
+ });
670
+ }
520
671
  // Serialize ALL wiki runs (boot, event-driven, manual) on one chain -- the
521
672
  // shared agent process rejects concurrent requests, so the boot run and a
522
673
  // manual trigger raced into 'Process is busy' (same pattern as the
523
674
  // dashboard agent's runDashboardAgent chain above).
524
675
  let wikiRunChain = Promise.resolve();
525
- const runWikiAgent = () => {
526
- wikiRunChain = wikiRunChain.then(doWikiRun).catch(() => { });
676
+ const runWikiAgent = (trigger) => {
677
+ wikiRunChain = wikiRunChain.then(() => doWikiRun(trigger)).catch(() => { });
527
678
  return wikiRunChain;
528
679
  };
529
680
  // Event-driven: compile when extraction completes. Trailing-edge debounce so
530
681
  // bursts of extraction:completed events coalesce into one wiki compile run.
531
- eventBus.onDebounced('extraction:completed', () => runWikiAgent(), 30_000);
682
+ eventBus.onDebounced('extraction:completed', () => runWikiAgent('extraction:completed'), 30_000);
532
683
  // Promotion feeds the wiki: freshly promoted decisions are exactly the
533
684
  // material daily notes and lessons compile from.
534
- eventBus.onDebounced('memory:promoted', () => runWikiAgent(), 30_000);
685
+ eventBus.onDebounced('memory:promoted', () => runWikiAgent('memory:promoted'), 30_000);
535
686
  // Emit agent:action notices when wiki pages are compiled
536
687
  eventBus.on('wiki:compiled', (event) => {
537
688
  if (event.type === 'wiki:compiled') {
@@ -547,11 +698,11 @@ This saves resources. Only compile when there is genuinely new information to do
547
698
  });
548
699
  // Manual trigger API
549
700
  apiServer.app.post('/api/wiki/compile', auth_middleware_js_1.requireAuth, async (_req, res) => {
550
- runWikiAgent().catch(() => { });
701
+ runWikiAgent('manual').catch(() => { });
551
702
  res.json({ ok: true, message: 'Wiki compilation triggered' });
552
703
  });
553
704
  // First run after 15s (let connectors and dashboard agent go first)
554
- setTimeout(runWikiAgent, 15_000);
705
+ setTimeout(() => runWikiAgent('boot'), 15_000);
555
706
  routesLogger.info('[Wiki Agent] Ready — triggers: extraction:completed event, POST /api/wiki/compile');
556
707
  }
557
708
  // -- Memory Promotion: scheduled observation->decision curation --
@@ -577,7 +728,19 @@ This saves resources. Only compile when there is genuinely new information to do
577
728
  'rules, recurring risk patterns; NEVER task lifecycle states, greetings, or logistics). ' +
578
729
  'Include scopes (the source channel, and the project when identifiable) and event_date.\n' +
579
730
  '5. Finish with exactly PROMOTED <n> or NO_UPDATE.';
580
- const doPromotionRun = async () => {
731
+ const doPromotionRun = async (opts) => {
732
+ // Stage-2 gate (function level, plan F4). Promotion never enters shadow
733
+ // - its mama_save writes have no capture seam.
734
+ if ((0, workorder_publishers_js_1.resolvePublishAction)(stage2Flag, 'memory-curation') === 'enqueue') {
735
+ try {
736
+ const now = Date.now();
737
+ enqueueWorkOrderOrThrow('memory-curation', opts?.manual ? (0, workorder_publishers_js_1.promotionManualKey)(now) : (0, workorder_publishers_js_1.promotionKey)(now), { scheduledAt: new Date(now).toISOString() }, opts?.manual ? 'high' : undefined);
738
+ }
739
+ catch (err) {
740
+ routesLogger.error('[stage2] promotion enqueue failed:', err instanceof Error ? err.message : err);
741
+ }
742
+ return;
743
+ }
581
744
  if (!toolExecutor.getAgentProcessManager()) {
582
745
  routesLogger.warn('[Memory Promotion] AgentProcessManager not available yet');
583
746
  return;
@@ -604,179 +767,113 @@ This saves resources. Only compile when there is genuinely new information to do
604
767
  routesLogger.error('[Memory Promotion] Error:', err instanceof Error ? err.message : err);
605
768
  }
606
769
  };
770
+ // Stage-2 promotion completion hook (plan E4): the PROMOTED <n> parse and
771
+ // its event emissions are the wiki ingress chain's second link - losing
772
+ // them in the workorder conversion would silently sever memory:promoted.
773
+ if (workOrderConsumer) {
774
+ const { buildPromotionAfterHook } = await import('../../operator/workorder-hooks.js');
775
+ workOrderConsumer.registerHook('memory-curation', {
776
+ after: buildPromotionAfterHook({
777
+ emitAgentAction: (action, target) => eventBus.emit({ type: 'agent:action', agent: 'Memory Agent', action, target }),
778
+ emitMemoryPromoted: (saved) => eventBus.emit({ type: 'memory:promoted', saved }),
779
+ log: (line) => console.log(line),
780
+ }),
781
+ });
782
+ }
607
783
  // Serialize all promotion runs on one chain (same 'Process is busy' class
608
784
  // as the dashboard and wiki agents).
609
785
  let promotionRunChain = Promise.resolve();
610
- const runMemoryPromotion = () => {
611
- promotionRunChain = promotionRunChain.then(doPromotionRun).catch(() => { });
786
+ const runMemoryPromotion = (opts) => {
787
+ promotionRunChain = promotionRunChain.then(() => doPromotionRun(opts)).catch(() => { });
612
788
  return promotionRunChain;
613
789
  };
614
- setTimeout(runMemoryPromotion, PROMOTION_INITIAL_DELAY_MS);
615
- setInterval(runMemoryPromotion, PROMOTION_INTERVAL_MS);
790
+ setTimeout(() => runMemoryPromotion(), PROMOTION_INITIAL_DELAY_MS);
791
+ setInterval(() => runMemoryPromotion(), PROMOTION_INTERVAL_MS);
616
792
  apiServer.app.post('/api/memory/promote', auth_middleware_js_1.requireAuth, (_req, res) => {
617
- runMemoryPromotion().catch(() => { });
793
+ runMemoryPromotion({ manual: true }).catch(() => { });
618
794
  res.json({ ok: true, message: 'Memory promotion run triggered' });
619
795
  });
620
796
  console.log(`[Memory Promotion] Ready: every ${PROMOTION_INTERVAL_MS / 3_600_000}h, POST /api/memory/promote`);
621
797
  }
622
- // ── Conductor Audit hourly system health check ──────────────────────
798
+ // -- System Audit: hourly deterministic code checks ---------------------
799
+ // Owner decision 2026-04-22 (mama_conductor_audit_code_based_read_only),
800
+ // landed 2026-07-17: the audit is fact collection and recording executed by
801
+ // CODE - no LLM invocation, no auto-fix, no broad filesystem access. The
802
+ // prior hourly LLM audit violated that decision and the 2026-05-14
803
+ // no-shell-autofix rule, and lost its tools entirely to the persona
804
+ // lockdown (--tools ""). The 24h alert-dedup contract (owner verdict
805
+ // 2026-07-11) is preserved inside runCodeAudit; MAJOR findings flow through
806
+ // dispatchSecurityAlertDirect to the configured security alert sender.
623
807
  const AUDIT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
624
808
  const AUDIT_INITIAL_DELAY_MS = 5 * 60 * 1000; // 5 min after startup
625
- // Owner verdict 2026-07-11: the hourly audit re-sent the same MAJOR alert every
626
- // run because each audit was stateless. agent_notices does not carry audit
627
- // findings (review feedback on #134), so dedup state lives in a JSON file the
628
- // conductor reads/writes itself, and the current time is interpolated per run
629
- // so the 24h comparison is grounded.
630
- const buildAuditPrompt = (nowIso) => 'Perform a system audit. Read ~/.mama/skills/audit-checklist.md and execute each step. ' +
631
- 'Classify findings as MINOR or MAJOR. ' +
632
- 'For MINOR items: execute the auto-fix command immediately (Bash curl). Do NOT just report — fix it. ' +
633
- 'For MAJOR items: report to human via channel alert, subject to the dedup rule below. ' +
634
- `Alert dedup (MANDATORY): the current time is ${nowIso}. Read ~/.mama/state/audit-findings.json ` +
635
- '(it may not exist on the first run). Re-alert a MAJOR finding only if it is NEW (no entry ' +
636
- 'with the same id), has ESCALATED, or its last_alerted_at is older than 24 hours compared to ' +
637
- 'the current time; otherwise stay silent about it. Never alert MINOR findings to the owner. ' +
638
- 'After the audit, Write ~/.mama/state/audit-findings.json with an array of every current ' +
639
- 'finding: {id (stable kebab-case slug), severity, first_seen, last_seen, last_alerted_at}. ' +
640
- 'Carry forward first_seen and last_alerted_at from the previous file; set last_alerted_at to ' +
641
- 'the current time only for findings you actually alerted this run. ' +
642
- // Owner verdict 2026-07-10: hourly-audit self-notes are memory noise -- the two
643
- // "durable policy" saves it produced were unscoped near-duplicates polluting
644
- // global recall. The audit record lives in validation sessions + agent_activity;
645
- // MAJOR findings reach the owner via the channel alert. No memory writes here.
646
- 'Do NOT call mama_save under any circumstances: the validation session and agent_activity ' +
647
- 'are the durable audit record, and repeated audits must not accumulate policy notes in memory.';
648
- const runConductorAudit = async () => {
649
- let auditSession = null;
650
- let auditVersion = 0;
651
- const auditStart = Date.now();
809
+ const doSystemAudit = async () => {
652
810
  try {
653
- routesLogger.debug('[Conductor Audit] Starting hourly audit...');
654
- try {
655
- const ver = sessionsDb ? (0, agent_store_js_1.getLatestVersion)(sessionsDb, 'conductor') : null;
656
- auditVersion = ver?.version ?? 0;
657
- auditSession =
658
- validationService?.startSession('conductor', auditVersion, 'audit', {
659
- goal: 'hourly system audit',
660
- }) ?? null;
661
- }
662
- catch (bootstrapErr) {
663
- routesLogger.warn('[Conductor Audit] Failed to initialize audit telemetry:', bootstrapErr);
664
- auditVersion = 0;
665
- auditSession = null;
666
- }
667
- if (sessionsDb) {
668
- try {
669
- const startRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
670
- agent_id: 'conductor',
671
- agent_version: auditVersion,
672
- type: 'audit_start',
673
- input_summary: 'Hourly system audit',
674
- run_id: auditSession?.id,
675
- execution_status: 'started',
676
- trigger_reason: 'audit',
811
+ const report = await (0, code_audit_js_1.runCodeAudit)({
812
+ config: {
813
+ telegram: config.telegram,
814
+ multi_agent: config.multi_agent,
815
+ roles: config.roles,
816
+ },
817
+ // Evaluated at audit time: gateway-wiring registers the sender only
818
+ // for ACTIVE gateways, so a configured target on a dead gateway must
819
+ // not report a false PASS.
820
+ securityAlertConfigured: (0, security_monitor_js_1.hasSecurityAlertSender)(),
821
+ // Direct dispatch, NOT recordSecurityEvent: audit findings are
822
+ // self-generated and must not fabricate incident/denylist/RDAP
823
+ // artifacts for a pseudo client. dispatchSecurityAlertDirect awaits
824
+ // delivery and throws on failure, so runCodeAudit keeps
825
+ // last_alerted_at null and retries on the next run (fail loud).
826
+ alert: async (finding, reason) => {
827
+ await (0, security_monitor_js_1.dispatchSecurityAlertDirect)({
828
+ type: 'system_audit_finding',
829
+ severity: 'critical',
830
+ message: `[Audit/${reason}] ${finding.summary}`,
831
+ // path carries the finding id so the alert-cooldown fingerprint
832
+ // distinguishes findings instead of collapsing them all into one.
833
+ path: finding.id,
834
+ details: { findingId: finding.id, detail: finding.detail ?? null },
677
835
  });
678
- if (auditSession) {
679
- try {
680
- validationService?.recordRun(auditSession.id, { activityId: startRow.id });
681
- }
682
- catch (telemetryErr) {
683
- routesLogger.warn('[Conductor Audit] Failed to link startup activity to validation session:', telemetryErr);
684
- }
685
- }
686
- }
687
- catch (telemetryErr) {
688
- routesLogger.warn('[Conductor Audit] Failed to write startup telemetry:', telemetryErr);
689
- }
690
- }
691
- const auditChannelId = `conductor-audit-${Date.now()}`;
692
- await messageRouter.process({
693
- source: 'system',
694
- channelId: auditChannelId,
695
- userId: 'system',
696
- text: buildAuditPrompt(new Date().toISOString()),
836
+ },
697
837
  });
698
- const auditDuration = Date.now() - auditStart;
699
- try {
700
- if (sessionsDb) {
701
- const activityRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
702
- agent_id: 'conductor',
703
- agent_version: auditVersion,
704
- type: 'audit_complete',
705
- input_summary: 'Hourly system audit',
706
- duration_ms: auditDuration,
707
- run_id: auditSession?.id,
708
- execution_status: 'completed',
709
- trigger_reason: 'audit',
710
- });
711
- if (auditSession) {
712
- validationService?.recordRun(auditSession.id, { activityId: activityRow.id });
713
- }
714
- }
715
- }
716
- catch (telemetryErr) {
717
- routesLogger.warn('[Conductor Audit] Failed to write completion telemetry:', telemetryErr);
718
- }
719
- try {
720
- if (auditSession && validationService) {
721
- validationService.finalizeSession(auditSession.id, {
722
- execution_status: 'completed',
723
- metrics: { duration_ms: auditDuration },
724
- });
725
- }
726
- }
727
- catch (telemetryErr) {
728
- routesLogger.warn('[Conductor Audit] Failed to finalize validation session:', telemetryErr);
729
- }
730
- routesLogger.debug('[Conductor Audit] Audit complete');
838
+ const majors = report.findings.filter((f) => f.severity === 'MAJOR').length;
839
+ const minors = report.findings.filter((f) => f.severity === 'MINOR').length;
840
+ routesLogger.info(`[System Audit] ${report.pass_items.length} pass, ${majors} MAJOR, ${minors} MINOR, ` +
841
+ `alerted=[${report.alerted.join(',')}] in ${report.duration_ms}ms`);
842
+ if (report.alert_delivery_failures.length > 0) {
843
+ routesLogger.error('[System Audit] Alert delivery failures:', report.alert_delivery_failures.join('; '));
844
+ }
845
+ return report;
731
846
  }
732
847
  catch (err) {
733
- const auditDuration = Date.now() - auditStart;
734
- try {
735
- if (sessionsDb) {
736
- const failRow = (0, agent_store_js_1.logActivity)(sessionsDb, {
737
- agent_id: 'conductor',
738
- agent_version: auditVersion,
739
- type: 'audit_failed',
740
- input_summary: 'Hourly system audit failed',
741
- error_message: err instanceof Error ? err.message : String(err),
742
- duration_ms: auditDuration,
743
- run_id: auditSession?.id,
744
- execution_status: 'failed',
745
- trigger_reason: 'audit',
746
- });
747
- if (auditSession) {
748
- validationService?.recordRun(auditSession.id, { activityId: failRow.id });
749
- }
750
- }
751
- }
752
- catch (telemetryErr) {
753
- routesLogger.warn('[Conductor Audit] Failed to write failure telemetry:', telemetryErr);
754
- }
755
- try {
756
- if (auditSession && validationService) {
757
- validationService.finalizeSession(auditSession.id, {
758
- execution_status: 'failed',
759
- error_message: err instanceof Error ? err.message : String(err),
760
- metrics: { duration_ms: auditDuration },
761
- });
762
- }
763
- }
764
- catch (telemetryErr) {
765
- routesLogger.warn('[Conductor Audit] Failed to finalize failed session:', telemetryErr);
766
- }
767
- routesLogger.error('[Conductor Audit] Failed:', err instanceof Error ? err.message : String(err));
848
+ routesLogger.error('[System Audit] Failed:', err instanceof Error ? err.message : String(err));
849
+ return null;
768
850
  }
769
851
  };
852
+ // Serialize scheduled and manual runs (same pattern as the memory-promotion
853
+ // chain above): overlapping audits would read the same prior state, alert
854
+ // twice, and race the audit-findings.json write. doSystemAudit never
855
+ // rejects, so chaining on the previous run is sufficient.
856
+ let auditRunChain = Promise.resolve(null);
857
+ const runSystemAudit = () => {
858
+ const next = auditRunChain.then(doSystemAudit);
859
+ auditRunChain = next;
860
+ return next;
861
+ };
770
862
  setTimeout(() => {
771
- runConductorAudit();
772
- setInterval(runConductorAudit, AUDIT_INTERVAL_MS);
863
+ void runSystemAudit();
864
+ setInterval(() => void runSystemAudit(), AUDIT_INTERVAL_MS);
773
865
  }, AUDIT_INITIAL_DELAY_MS);
774
- // Manual trigger
866
+ // Manual trigger returns the full report (read-only, no LLM, fast)
775
867
  apiServer.app.post('/api/conductor/audit', auth_middleware_js_1.requireAuth, async (_req, res) => {
776
- runConductorAudit().catch(() => { });
777
- res.json({ ok: true, message: 'Conductor audit triggered' });
868
+ const report = await runSystemAudit();
869
+ if (report) {
870
+ res.json({ ok: true, mode: 'code', report });
871
+ }
872
+ else {
873
+ res.status(500).json({ ok: false, error: 'audit failed - see daemon.log' });
874
+ }
778
875
  });
779
- routesLogger.info('[Conductor Audit] Ready runs every 60 min, POST /api/conductor/audit for manual trigger');
876
+ routesLogger.info('[System Audit] Ready - deterministic code checks every 60 min, POST /api/conductor/audit for manual run');
780
877
  // ── Memory Agent stats API ────────────────────────────────────────────
781
878
  apiServer.app.get('/api/memory-agent/stats', auth_middleware_js_1.requireAuth, (_req, res) => {
782
879
  const stats = messageRouter.getMemoryAgentStats();