reflectt-node 0.1.21 → 0.1.22

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 (103) hide show
  1. package/README.md +64 -0
  2. package/dist/canvas-interactive.d.ts +53 -0
  3. package/dist/canvas-interactive.d.ts.map +1 -1
  4. package/dist/canvas-interactive.js +194 -0
  5. package/dist/canvas-interactive.js.map +1 -1
  6. package/dist/canvas-push.d.ts +2 -0
  7. package/dist/canvas-push.d.ts.map +1 -1
  8. package/dist/canvas-push.js +89 -0
  9. package/dist/canvas-push.js.map +1 -1
  10. package/dist/channels.d.ts +1 -1
  11. package/dist/intervention-template.d.ts +67 -0
  12. package/dist/intervention-template.d.ts.map +1 -0
  13. package/dist/intervention-template.js +290 -0
  14. package/dist/intervention-template.js.map +1 -0
  15. package/dist/request-tracker.d.ts +1 -0
  16. package/dist/request-tracker.d.ts.map +1 -1
  17. package/dist/request-tracker.js +16 -4
  18. package/dist/request-tracker.js.map +1 -1
  19. package/dist/server.d.ts.map +1 -1
  20. package/dist/server.js +144 -3
  21. package/dist/server.js.map +1 -1
  22. package/dist/stall-detector.d.ts +109 -0
  23. package/dist/stall-detector.d.ts.map +1 -0
  24. package/dist/stall-detector.js +279 -0
  25. package/dist/stall-detector.js.map +1 -0
  26. package/package.json +1 -1
  27. package/public/docs.md +9 -0
  28. package/dist/agent-config.test.d.ts +0 -2
  29. package/dist/agent-config.test.d.ts.map +0 -1
  30. package/dist/agent-config.test.js +0 -91
  31. package/dist/agent-config.test.js.map +0 -1
  32. package/dist/agent-exec-guardrail.test.d.ts +0 -2
  33. package/dist/agent-exec-guardrail.test.d.ts.map +0 -1
  34. package/dist/agent-exec-guardrail.test.js +0 -55
  35. package/dist/agent-exec-guardrail.test.js.map +0 -1
  36. package/dist/agent-memories.test.d.ts +0 -2
  37. package/dist/agent-memories.test.d.ts.map +0 -1
  38. package/dist/agent-memories.test.js +0 -327
  39. package/dist/agent-memories.test.js.map +0 -1
  40. package/dist/agent-messaging.test.d.ts +0 -2
  41. package/dist/agent-messaging.test.d.ts.map +0 -1
  42. package/dist/agent-messaging.test.js +0 -105
  43. package/dist/agent-messaging.test.js.map +0 -1
  44. package/dist/agent-runs.test.d.ts +0 -2
  45. package/dist/agent-runs.test.d.ts.map +0 -1
  46. package/dist/agent-runs.test.js +0 -386
  47. package/dist/agent-runs.test.js.map +0 -1
  48. package/dist/api.test.d.ts +0 -2
  49. package/dist/api.test.d.ts.map +0 -1
  50. package/dist/api.test.js +0 -99
  51. package/dist/api.test.js.map +0 -1
  52. package/dist/approval-queue.test.d.ts +0 -2
  53. package/dist/approval-queue.test.d.ts.map +0 -1
  54. package/dist/approval-queue.test.js +0 -118
  55. package/dist/approval-queue.test.js.map +0 -1
  56. package/dist/artifact-store.test.d.ts +0 -2
  57. package/dist/artifact-store.test.d.ts.map +0 -1
  58. package/dist/artifact-store.test.js +0 -119
  59. package/dist/artifact-store.test.js.map +0 -1
  60. package/dist/canvas-input.test.d.ts +0 -2
  61. package/dist/canvas-input.test.d.ts.map +0 -1
  62. package/dist/canvas-input.test.js +0 -96
  63. package/dist/canvas-input.test.js.map +0 -1
  64. package/dist/canvas-render.test.d.ts +0 -2
  65. package/dist/canvas-render.test.d.ts.map +0 -1
  66. package/dist/canvas-render.test.js +0 -95
  67. package/dist/canvas-render.test.js.map +0 -1
  68. package/dist/e2e-loop-proof.test.d.ts +0 -2
  69. package/dist/e2e-loop-proof.test.d.ts.map +0 -1
  70. package/dist/e2e-loop-proof.test.js +0 -114
  71. package/dist/e2e-loop-proof.test.js.map +0 -1
  72. package/dist/email-sms-send.test.d.ts +0 -2
  73. package/dist/email-sms-send.test.d.ts.map +0 -1
  74. package/dist/email-sms-send.test.js +0 -96
  75. package/dist/email-sms-send.test.js.map +0 -1
  76. package/dist/handoff-state.test.d.ts +0 -2
  77. package/dist/handoff-state.test.d.ts.map +0 -1
  78. package/dist/handoff-state.test.js +0 -102
  79. package/dist/handoff-state.test.js.map +0 -1
  80. package/dist/routing-enforcement.test.d.ts +0 -2
  81. package/dist/routing-enforcement.test.d.ts.map +0 -1
  82. package/dist/routing-enforcement.test.js +0 -62
  83. package/dist/routing-enforcement.test.js.map +0 -1
  84. package/dist/run-retention.test.d.ts +0 -2
  85. package/dist/run-retention.test.d.ts.map +0 -1
  86. package/dist/run-retention.test.js +0 -57
  87. package/dist/run-retention.test.js.map +0 -1
  88. package/dist/run-stream.test.d.ts +0 -2
  89. package/dist/run-stream.test.d.ts.map +0 -1
  90. package/dist/run-stream.test.js +0 -70
  91. package/dist/run-stream.test.js.map +0 -1
  92. package/dist/webhook-storage.test.d.ts +0 -2
  93. package/dist/webhook-storage.test.d.ts.map +0 -1
  94. package/dist/webhook-storage.test.js +0 -86
  95. package/dist/webhook-storage.test.js.map +0 -1
  96. package/dist/workflow-canvas-state.test.d.ts +0 -2
  97. package/dist/workflow-canvas-state.test.d.ts.map +0 -1
  98. package/dist/workflow-canvas-state.test.js +0 -53
  99. package/dist/workflow-canvas-state.test.js.map +0 -1
  100. package/dist/workflow-templates.test.d.ts +0 -2
  101. package/dist/workflow-templates.test.d.ts.map +0 -1
  102. package/dist/workflow-templates.test.js +0 -76
  103. package/dist/workflow-templates.test.js.map +0 -1
package/dist/server.js CHANGED
@@ -12,6 +12,8 @@ import { promises as fs, existsSync, readFileSync, readdirSync } from 'fs';
12
12
  import { resolve, sep, join } from 'path';
13
13
  import { execSync } from 'child_process';
14
14
  import { serverConfig, openclawConfig, isDev, REFLECTT_HOME, DATA_DIR } from './config.js';
15
+ import { getStallDetector, emitWorkflowStall, onStallEvent } from './stall-detector.js';
16
+ import { processStallEvent } from './intervention-template.js';
15
17
  import { trackRequest, getRequestMetrics } from './request-tracker.js';
16
18
  import { getPreflightMetrics, snapshotDailyMetrics, getDailySnapshots, startAutoSnapshot } from './alert-preflight.js';
17
19
  // ── Build info (read once at startup) ──────────────────────────────────────
@@ -325,6 +327,7 @@ const UpdateTaskSchema = z.object({
325
327
  assignee: z.string().optional(),
326
328
  reviewer: z.string().optional(),
327
329
  done_criteria: z.array(z.string().min(1)).optional(),
330
+ criteria_verified: z.boolean().optional(), // bypass done_criteria gate for todo→validating
328
331
  priority: z.enum(['P0', 'P1', 'P2', 'P3']).optional(),
329
332
  blocked_by: z.array(z.string()).optional(),
330
333
  epic_id: z.string().optional(),
@@ -2435,6 +2438,82 @@ export async function createServer() {
2435
2438
  // Start self-keepalive to prevent container eviction in serverless environments
2436
2439
  const serverPort = Number(process.env['PORT'] || process.env['REFLECTT_PORT'] || 4445);
2437
2440
  startSelfKeepalive(serverPort);
2441
+ // ── Stall Detector ─────────────────────────────────────────────────────────
2442
+ const sd = getStallDetector();
2443
+ // Register stall event handler: compile intervention and post to chat
2444
+ onStallEvent((event) => {
2445
+ // Adapt stall-detector event to intervention-template expected format
2446
+ const adaptedEvent = {
2447
+ stallId: event.stallId,
2448
+ userId: event.userId,
2449
+ stallType: event.stallType,
2450
+ personalizations: {
2451
+ user_name: event.userId,
2452
+ last_intent: event.context?.lastAction,
2453
+ active_task_title: event.context?.lastAction,
2454
+ last_agent_name: event.context?.lastAgent,
2455
+ },
2456
+ timestamp: event.timestamp,
2457
+ };
2458
+ const result = processStallEvent(adaptedEvent);
2459
+ if (!result.sent) {
2460
+ console.debug('[StallDetector] Intervention not sent:', result.reason);
2461
+ return;
2462
+ }
2463
+ // Select an agent to send the intervention (use lastAgent from context, or default)
2464
+ const agentName = event.context?.lastAgent || 'rhythm';
2465
+ const message = result.message || 'Hey! Just checking in — want to pick up where you left off?';
2466
+ // Post to #general as the intervening agent
2467
+ const baseUrl = `http://127.0.0.1:${serverPort}`;
2468
+ fetch(`${baseUrl}/chat/messages`, {
2469
+ method: 'POST',
2470
+ headers: { 'Content-Type': 'application/json', 'x-reflectt-internal': 'true' },
2471
+ body: JSON.stringify({ from: agentName, channel: 'general', content: message }),
2472
+ }).catch((err) => console.error('[StallDetector] Failed to post intervention:', err));
2473
+ });
2474
+ // Start stall detector if enabled in config
2475
+ const stallConfig = serverConfig.stallDetector;
2476
+ if (stallConfig?.enabled)
2477
+ sd.start();
2478
+ app.get('/stall-detector', async () => {
2479
+ return {
2480
+ enabled: sd.getAllStates().length > 0,
2481
+ states: sd.getAllStates().map(s => ({
2482
+ userId: s.userId,
2483
+ phase: s.phase,
2484
+ context: s.context,
2485
+ stallFired: [...s.stallFired],
2486
+ })),
2487
+ };
2488
+ });
2489
+ app.post('/stall-detector/config', async (request) => {
2490
+ const body = (request.body ?? {});
2491
+ // Accept: { enabled, thresholds: { newUserMinutes, inSessionMinutes, setupMinutes } }
2492
+ const newCfg = {};
2493
+ if (typeof body.enabled === 'boolean') {
2494
+ newCfg.enabled = body.enabled;
2495
+ }
2496
+ if (body.thresholds && typeof body.thresholds === 'object') {
2497
+ newCfg.thresholds = body.thresholds;
2498
+ }
2499
+ // Merge into serverConfig
2500
+ ;
2501
+ serverConfig.stallDetector = {
2502
+ ...(serverConfig.stallDetector ?? {}),
2503
+ ...newCfg,
2504
+ };
2505
+ if (newCfg.enabled)
2506
+ sd.start();
2507
+ return { success: true, config: serverConfig.stallDetector };
2508
+ });
2509
+ app.post('/stall-detector/test', async (request) => {
2510
+ // Fire a test stall event for a given userId
2511
+ const { userId } = (request.body ?? {});
2512
+ if (!userId)
2513
+ return { success: false, error: 'userId required' };
2514
+ sd.recordActivity(userId, { phase: 'new_user' });
2515
+ return { success: true, message: `Recorded activity for ${userId}` };
2516
+ });
2438
2517
  // Self-keepalive status + warm boot info
2439
2518
  app.get('/health/keepalive', async () => {
2440
2519
  return getSelfKeepaliveStatus();
@@ -3549,6 +3628,8 @@ export async function createServer() {
3549
3628
  if (data.from) {
3550
3629
  presenceManager.recordActivity(data.from, 'message');
3551
3630
  presenceManager.touchPresence(data.from);
3631
+ // Stall detector: user sent a message — record activity
3632
+ getStallDetector().recordActivity(data.from);
3552
3633
  // Activation funnel: first team message
3553
3634
  emitActivationEvent('first_team_message_sent', data.from, {
3554
3635
  channel: data.channel || 'general',
@@ -7984,7 +8065,8 @@ export async function createServer() {
7984
8065
  'in-progress': ['blocked', 'validating', 'done', 'doing', 'todo', 'cancelled'], // legacy, permissive
7985
8066
  };
7986
8067
  const allowed = ALLOWED_TRANSITIONS[existing.status] ?? [];
7987
- if (!allowed.includes(parsed.status)) {
8068
+ // Allow todo→validating when criteria_verified=true
8069
+ if (!allowed.includes(parsed.status) && !(parsed.status === 'validating' && existing.status === 'todo' && parsed.criteria_verified === true)) {
7988
8070
  const meta = (incomingMeta ?? {});
7989
8071
  const isReopen = meta.reopen === true;
7990
8072
  const reopenReason = typeof meta.reopen_reason === 'string' ? String(meta.reopen_reason).trim() : '';
@@ -8004,6 +8086,21 @@ export async function createServer() {
8004
8086
  mergedMeta.reopen_reason = reopenReason;
8005
8087
  mergedMeta.reopened_at = Date.now();
8006
8088
  mergedMeta.reopened_from = existing.status;
8089
+ // ── Done-criteria verification gate ──
8090
+ // Block todo→validating unless criteria_verified=true is set.
8091
+ if (parsed.status === 'validating' && existing.status === 'todo') {
8092
+ const hasDoneCriteria = Boolean(existing.done_criteria && existing.done_criteria.length > 0);
8093
+ if (hasDoneCriteria && parsed.criteria_verified !== true) {
8094
+ const dc = Array.isArray(existing.done_criteria) ? existing.done_criteria.length : 0;
8095
+ reply.code(422);
8096
+ return {
8097
+ success: false,
8098
+ error: `All ${dc} done criteria must be verified. Set criteria_verified=true in PATCH body to unblock.`,
8099
+ code: 'DONE_CRITERIA_NOT_VERIFIED',
8100
+ gate: 'done_criteria_verification',
8101
+ };
8102
+ }
8103
+ }
8007
8104
  // Emit trust signal: forced state bypass
8008
8105
  const NORMAL_ESCALATION_PATHS = ['todo→doing', 'doing→validating', 'validating→done'];
8009
8106
  const jumpPath = `${existing.status}→${parsed.status}`;
@@ -8495,6 +8592,19 @@ export async function createServer() {
8495
8592
  reply.code(404);
8496
8593
  return { success: false, error: 'Task not found' };
8497
8594
  }
8595
+ // ── Emit workflow stall when task enters review ──
8596
+ if (effectiveTargetStatus === 'validating' &&
8597
+ existing.status !== 'validating' &&
8598
+ existing.status !== 'done') {
8599
+ const reviewer = task.reviewer;
8600
+ if (reviewer) {
8601
+ emitWorkflowStall(reviewer, 'review_pending', {
8602
+ lastAction: `task "${task.title}" submitted for review`,
8603
+ lastAgent: task.assignee || 'unknown',
8604
+ lastActionAt: Date.now(),
8605
+ });
8606
+ }
8607
+ }
8498
8608
  // ── Audit ledger: log review-field mutations ──
8499
8609
  {
8500
8610
  const oldMeta = (existing.metadata || {});
@@ -8549,6 +8659,10 @@ export async function createServer() {
8549
8659
  if (parsed.status === 'done') {
8550
8660
  presenceManager.recordActivity(task.assignee, 'task_completed');
8551
8661
  presenceManager.updatePresence(task.assignee, 'working', null);
8662
+ // Stall detector: agent completed a task — user should respond
8663
+ // Determine who to notify: the task creator / assignee who might be waiting
8664
+ const waitingUserId = task.metadata?.userId || task.assignee;
8665
+ getStallDetector().recordAgentResponse(waitingUserId, task.assignee);
8552
8666
  trackTaskEvent('completed');
8553
8667
  }
8554
8668
  else if (parsed.status === 'doing') {
@@ -10137,11 +10251,13 @@ export async function createServer() {
10137
10251
  // ── Canvas interactive routes (extracted to src/canvas-interactive.ts) ─────
10138
10252
  // POST /canvas/gaze, POST /canvas/briefing, POST /canvas/victory,
10139
10253
  // POST /canvas/spark, POST /canvas/express, GET /canvas/render/stream
10140
- const { canvasInteractiveRoutes } = await import("./canvas-interactive.js");
10254
+ const { canvasInteractiveRoutes, registerCapabilityRoutes } = await import("./canvas-interactive.js");
10141
10255
  await app.register(canvasInteractiveRoutes, {
10142
10256
  eventBus,
10143
10257
  canvasStateMap,
10144
10258
  });
10259
+ // Register capability routes: GET/POST /canvas/capability
10260
+ registerCapabilityRoutes(app);
10145
10261
  // ── Canvas activity stream — SSE with backfill ────────────────────────
10146
10262
  // New viewers get the last 20 canvas events immediately on connect (backfill),
10147
10263
  // then receive live events going forward. Canvas feels alive from frame 1.
@@ -10314,6 +10430,7 @@ export async function createServer() {
10314
10430
  await app.register(canvasPushRoutes, {
10315
10431
  eventBus,
10316
10432
  queueCanvasPushEvent,
10433
+ canvasStateMap,
10317
10434
  });
10318
10435
  // GET /canvas/pulse — SSE stream emitting a heartbeat tick every 2s with live intensity values
10319
10436
  // Drives smooth canvas animation without polling. Each tick includes per-agent orb data + team mood.
@@ -10359,6 +10476,28 @@ export async function createServer() {
10359
10476
  }
10360
10477
  catch { /* non-blocking */ }
10361
10478
  };
10479
+ let canvasMetaCache = null;
10480
+ const getCanvasMeta = () => {
10481
+ const now = Date.now();
10482
+ if (canvasMetaCache && (now - canvasMetaCache.age) < 30_000)
10483
+ return canvasMetaCache;
10484
+ const focus = getFocus();
10485
+ let upcomingEvents = [];
10486
+ try {
10487
+ const events = calendarEvents.listEvents({ from: now, to: now + 24 * 60 * 60 * 1000, limit: 5 });
10488
+ upcomingEvents = events.map(e => ({ id: e.id, summary: e.summary, dtstart: e.dtstart, organizer: e.organizer }));
10489
+ }
10490
+ catch { /* skip */ }
10491
+ let recentActivity = [];
10492
+ try {
10493
+ const twoHoursAgo = now - 2 * 60 * 60 * 1000;
10494
+ const activity = queryActivity({ range: '24h', type: ['task', 'chat'], limit: 10 });
10495
+ recentActivity = activity.events.filter(e => e.ts_ms > twoHoursAgo).slice(0, 5).map(e => ({ ts: e.ts_ms, type: e.type, subject: e.subject }));
10496
+ }
10497
+ catch { /* skip */ }
10498
+ canvasMetaCache = { focus, upcomingEvents, recentActivity, age: now };
10499
+ return canvasMetaCache;
10500
+ };
10362
10501
  const emitTick = () => {
10363
10502
  if (closed)
10364
10503
  return;
@@ -10411,10 +10550,12 @@ export async function createServer() {
10411
10550
  break;
10412
10551
  }
10413
10552
  }
10553
+ // Include focus, calendar, and activity (cached, 30s TTL)
10554
+ const meta = getCanvasMeta();
10414
10555
  const tick = {
10415
10556
  t: now,
10416
10557
  agents,
10417
- team: { rhythm, tension, ambientPulse, dominantColor },
10558
+ team: { rhythm, tension, ambientPulse, dominantColor, ...meta },
10418
10559
  };
10419
10560
  try {
10420
10561
  reply.raw.write(`data: ${JSON.stringify(tick)}\n\n`);