@slycode/slycode 0.2.3 → 0.2.5

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 (100) hide show
  1. package/dist/bridge/api.js +1 -1
  2. package/dist/bridge/api.js.map +1 -1
  3. package/dist/bridge/session-manager.js +54 -14
  4. package/dist/bridge/session-manager.js.map +1 -1
  5. package/dist/bridge/types.d.ts +6 -0
  6. package/dist/data/scaffold-templates/seed-cards-existing.json +8 -0
  7. package/dist/data/scaffold-templates/seed-cards-new.json +8 -0
  8. package/dist/scripts/scaffold.js +29 -7
  9. package/dist/web/.next/BUILD_ID +1 -1
  10. package/dist/web/.next/build-manifest.json +2 -2
  11. package/dist/web/.next/server/app/_global-error.html +2 -2
  12. package/dist/web/.next/server/app/_global-error.rsc +1 -1
  13. package/dist/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  14. package/dist/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/dist/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  16. package/dist/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  17. package/dist/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/dist/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/dist/web/.next/server/app/_not-found.html +1 -1
  20. package/dist/web/.next/server/app/_not-found.rsc +2 -2
  21. package/dist/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  22. package/dist/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  23. package/dist/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  24. package/dist/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  25. package/dist/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  26. package/dist/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  27. package/dist/web/.next/server/app/api/bridge/[...path]/route.js +1 -1
  28. package/dist/web/.next/server/app/api/bridge/[...path]/route.js.nft.json +1 -1
  29. package/dist/web/.next/server/app/api/kanban/route.js +4 -4
  30. package/dist/web/.next/server/app/api/kanban/route.js.nft.json +1 -1
  31. package/dist/web/.next/server/app/api/kanban/stream/route.js +1 -1
  32. package/dist/web/.next/server/app/api/kanban/stream/route.js.nft.json +1 -1
  33. package/dist/web/.next/server/app/api/scheduler/route.js +1 -1
  34. package/dist/web/.next/server/app/api/scheduler/route.js.nft.json +1 -1
  35. package/dist/web/.next/server/app/api/sly-actions/stream/route.js +1 -1
  36. package/dist/web/.next/server/app/api/sly-actions/stream/route.js.nft.json +1 -1
  37. package/dist/web/.next/server/app/page_client-reference-manifest.js +1 -1
  38. package/dist/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
  39. package/dist/web/.next/server/chunks/{[externals]__3d917172._.js → [externals]__65615fd8._.js} +2 -2
  40. package/dist/web/.next/server/chunks/[root-of-the-server]__09aec55a._.js +1 -1
  41. package/dist/web/.next/server/chunks/[root-of-the-server]__198f01e0._.js +1 -1
  42. package/dist/web/.next/server/chunks/[root-of-the-server]__2b639eab._.js +1 -1
  43. package/dist/web/.next/server/chunks/[root-of-the-server]__3b9d3e43._.js +1 -1
  44. package/dist/web/.next/server/chunks/[root-of-the-server]__3f239285._.js +1 -1
  45. package/dist/web/.next/server/chunks/[root-of-the-server]__46b023d4._.js +1 -1
  46. package/dist/web/.next/server/chunks/[root-of-the-server]__4c7995bf._.js +1 -1
  47. package/dist/web/.next/server/chunks/[root-of-the-server]__4d0d3464._.js +1 -1
  48. package/dist/web/.next/server/chunks/[root-of-the-server]__6d330d40._.js +1 -1
  49. package/dist/web/.next/server/chunks/[root-of-the-server]__6ffce934._.js +3 -0
  50. package/dist/web/.next/server/chunks/[root-of-the-server]__884d73e4._.js +1 -1
  51. package/dist/web/.next/server/chunks/[root-of-the-server]__8ab096a3._.js +3 -0
  52. package/dist/web/.next/server/chunks/[root-of-the-server]__9058a007._.js +1 -1
  53. package/dist/web/.next/server/chunks/[root-of-the-server]__933e6077._.js +2 -2
  54. package/dist/web/.next/server/chunks/[root-of-the-server]__949bb248._.js +1 -1
  55. package/dist/web/.next/server/chunks/[root-of-the-server]__aa814a86._.js +1 -1
  56. package/dist/web/.next/server/chunks/[root-of-the-server]__ad64e04f._.js +2 -2
  57. package/dist/web/.next/server/chunks/[root-of-the-server]__b90bbd70._.js +1 -1
  58. package/dist/web/.next/server/chunks/{[root-of-the-server]__4244617a._.js → [root-of-the-server]__b9e7d34c._.js} +4 -4
  59. package/dist/web/.next/server/chunks/[root-of-the-server]__baa99257._.js +1 -1
  60. package/dist/web/.next/server/chunks/[root-of-the-server]__bbb4b3ac._.js +1 -1
  61. package/dist/web/.next/server/chunks/[root-of-the-server]__bc55c42a._.js +2 -2
  62. package/dist/web/.next/server/chunks/[root-of-the-server]__bf286c26._.js +1 -1
  63. package/dist/web/.next/server/chunks/[root-of-the-server]__d219e3f0._.js +18 -0
  64. package/dist/web/.next/server/chunks/[root-of-the-server]__d38c7a96._.js +1 -1
  65. package/dist/web/.next/server/chunks/[root-of-the-server]__e88a19d2._.js +1 -1
  66. package/dist/web/.next/server/chunks/[root-of-the-server]__e9b0e744._.js +1 -1
  67. package/dist/web/.next/server/chunks/[root-of-the-server]__f1fe18e6._.js +2 -2
  68. package/dist/web/.next/server/chunks/[root-of-the-server]__f4124388._.js +3 -0
  69. package/dist/web/.next/server/chunks/[root-of-the-server]__f59af2bc._.js +1 -1
  70. package/dist/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_ffc6c790.js +4 -4
  71. package/dist/web/.next/server/chunks/src_677020aa._.js +1 -1
  72. package/dist/web/.next/server/chunks/src_lib_scheduler_ts_03988e3e._.js +1 -1
  73. package/dist/web/.next/server/chunks/src_lib_scheduler_ts_7120457c._.js +1 -1
  74. package/dist/web/.next/server/chunks/ssr/src_components_Dashboard_tsx_efc4dc27._.js +1 -1
  75. package/dist/web/.next/server/chunks/ssr/src_lib_registry_ts_2fc87c9c._.js +1 -1
  76. package/dist/web/.next/server/pages/404.html +1 -1
  77. package/dist/web/.next/server/pages/500.html +2 -2
  78. package/dist/web/.next/static/chunks/de965b463aff71a9.js +1 -0
  79. package/dist/web/.next/static/chunks/f3d7065d54a0b9ac.css +1 -0
  80. package/dist/web/src/app/api/bridge/[...path]/route.ts +3 -1
  81. package/dist/web/src/app/api/dashboard/route.ts +2 -1
  82. package/dist/web/src/app/api/search/route.ts +2 -1
  83. package/dist/web/src/app/api/transcribe/route.ts +6 -1
  84. package/dist/web/src/components/AddProjectModal.tsx +67 -35
  85. package/dist/web/src/lib/paths.ts +21 -0
  86. package/dist/web/src/lib/registry.ts +2 -1
  87. package/dist/web/src/lib/scheduler.ts +130 -30
  88. package/dist/web/tsconfig.tsbuildinfo +1 -1
  89. package/package.json +1 -1
  90. package/templates/kanban-seed.json +1 -1
  91. package/dist/data/scaffold-templates/seed-cards.json +0 -20
  92. package/dist/web/.next/server/chunks/[root-of-the-server]__289ec56e._.js +0 -3
  93. package/dist/web/.next/server/chunks/[root-of-the-server]__543058c2._.js +0 -18
  94. package/dist/web/.next/server/chunks/[root-of-the-server]__949d814c._.js +0 -3
  95. package/dist/web/.next/server/chunks/[root-of-the-server]__ce429522._.js +0 -3
  96. package/dist/web/.next/static/chunks/f4999889f2006fea.css +0 -1
  97. package/dist/web/.next/static/chunks/fbbdd316640f1e15.js +0 -1
  98. /package/dist/web/.next/static/{awhR2yoOiaXMMqO3AHnAQ → Ky5DLflBRa2FbKsyhH40R}/_buildManifest.js +0 -0
  99. /package/dist/web/.next/static/{awhR2yoOiaXMMqO3AHnAQ → Ky5DLflBRa2FbKsyhH40R}/_clientMiddlewareManifest.json +0 -0
  100. /package/dist/web/.next/static/{awhR2yoOiaXMMqO3AHnAQ → Ky5DLflBRa2FbKsyhH40R}/_ssgManifest.js +0 -0
@@ -64,3 +64,24 @@ export function getProjectPath(projectId: string, projectPath?: string): string
64
64
  if (projectPath) return projectPath;
65
65
  return path.join(getProjectsDir(), projectId.replace(/-/g, '_'));
66
66
  }
67
+
68
+ /**
69
+ * Get the bridge URL for server-side HTTP calls.
70
+ *
71
+ * Single source of truth. All server-side code that talks to the bridge
72
+ * should call this instead of reading process.env.BRIDGE_URL directly.
73
+ *
74
+ * In production, startup scripts (sly-start.sh, systemd, slycode CLI)
75
+ * set BRIDGE_URL in the shell environment before starting the web server.
76
+ *
77
+ * In dev, BRIDGE_URL is NOT set in the shell — the bridge runs on its
78
+ * hardcoded default (3004). But the parent .env has BRIDGE_URL=...7592
79
+ * for prod, and various .env loaders can leak that into process.env.
80
+ * So in dev we ignore process.env.BRIDGE_URL and use the known default.
81
+ */
82
+ export function getBridgeUrl(): string {
83
+ if (process.env.NODE_ENV === 'production' && process.env.BRIDGE_URL) {
84
+ return process.env.BRIDGE_URL;
85
+ }
86
+ return 'http://127.0.0.1:3004';
87
+ }
@@ -29,6 +29,7 @@ import {
29
29
  } from './asset-scanner';
30
30
  import { getStoreAssets } from './store-scanner';
31
31
  import { calculateHealthFromAssets } from './health-score';
32
+ import { getBridgeUrl } from './paths';
32
33
 
33
34
  // Path to the registry file
34
35
  // Resolution: SLYCODE_HOME → derive from cwd
@@ -306,7 +307,7 @@ export async function loadDashboardData(): Promise<DashboardData> {
306
307
  }, 0);
307
308
 
308
309
  // Fetch bridge session counts (best-effort, don't fail if bridge is down)
309
- const bridgeUrl = process.env.BRIDGE_URL || 'http://127.0.0.1:7592';
310
+ const bridgeUrl = getBridgeUrl();
310
311
  try {
311
312
  const resp = await fetch(`${bridgeUrl}/stats`, { signal: AbortSignal.timeout(2000) });
312
313
  if (resp.ok) {
@@ -12,23 +12,17 @@
12
12
  import { promises as fs } from 'fs';
13
13
  import { readFileSync } from 'fs';
14
14
  import path from 'path';
15
+ import os from 'os';
15
16
  import { Cron } from 'croner';
16
17
  import type { KanbanCard, KanbanBoard, AutomationConfig } from './types';
17
18
  import { loadRegistry } from './registry';
18
19
  import { cronToHumanReadable } from './cron-utils';
19
- import { getSlycodeRoot } from './paths';
20
+ import { getSlycodeRoot, getBridgeUrl } from './paths';
20
21
 
21
22
  /**
22
23
  * Load env vars from the project root .env file if not already set.
23
24
  * Next.js only auto-loads .env from web/, but our config lives in the parent.
24
- *
25
- * BRIDGE_URL and BRIDGE_PORT are excluded — these are port-dependent and differ
26
- * between dev (3004) and prod (7592). In prod, sly-start.sh sets BRIDGE_URL in
27
- * the shell environment. In dev, we fall back to localhost:3004. Loading them
28
- * from .env would route dev scheduler requests to the prod bridge.
29
25
  */
30
- const SKIP_ENV_KEYS = new Set(['BRIDGE_URL', 'BRIDGE_PORT']);
31
-
32
26
  function loadParentEnv() {
33
27
  try {
34
28
  const envPath = path.join(getSlycodeRoot(), '.env');
@@ -40,7 +34,6 @@ function loadParentEnv() {
40
34
  if (eqIdx < 0) continue;
41
35
  const key = trimmed.slice(0, eqIdx).trim();
42
36
  const value = trimmed.slice(eqIdx + 1).trim();
43
- if (SKIP_ENV_KEYS.has(key)) continue;
44
37
  if (!process.env[key]) {
45
38
  process.env[key] = value;
46
39
  }
@@ -50,12 +43,15 @@ function loadParentEnv() {
50
43
 
51
44
  loadParentEnv();
52
45
 
53
- const BRIDGE_URL = process.env.BRIDGE_URL || 'http://localhost:3004';
46
+ const BRIDGE_URL = getBridgeUrl();
54
47
  const CONFIGURED_TIMEZONE = process.env.TZ || 'UTC';
55
48
  const CHECK_INTERVAL_MS = 30_000; // Check every 30 seconds
56
49
  const GRACE_WINDOW_MS = 60_000; // Only catch up ticks missed within this window
57
50
  const FETCH_TIMEOUT_MS = 10_000; // Timeout for bridge HTTP calls
58
51
 
52
+ const AUTOMATION_LOG_PATH = path.join(os.homedir(), '.slycode', 'logs', 'automation.log');
53
+ const AUTOMATION_LOG_MAX_BYTES = 1_000_000; // 1MB cap
54
+
59
55
  // Fresh session path: simple liveness check after startup
60
56
  const LIVENESS_CHECK_MS = 20_000; // Wait 20s then check if session is alive
61
57
 
@@ -77,6 +73,59 @@ async function fetchWithTimeout(url: string, opts?: RequestInit): Promise<Respon
77
73
  }
78
74
  }
79
75
 
76
+ /**
77
+ * Automation run log entry — one per automation execution.
78
+ */
79
+ interface AutomationLogEntry {
80
+ timestamp: string;
81
+ cardId: string;
82
+ cardTitle: string;
83
+ projectId: string;
84
+ trigger: 'scheduled' | 'manual';
85
+ provider: string;
86
+ sessionName: string;
87
+ fresh: boolean;
88
+ bridgeRequest: { status: number; resumed?: boolean; pid?: number; error?: string } | null;
89
+ livenessCheck: { type: string; result: string; delayMs?: number; exitCode?: number; exitedAt?: string } | null;
90
+ outcome: 'success' | 'error';
91
+ error: string | null;
92
+ elapsedMs: number;
93
+ }
94
+
95
+ /**
96
+ * Append a JSON lines entry to the automation log.
97
+ * Rotates by dropping the oldest half when the file exceeds 1MB.
98
+ */
99
+ async function writeAutomationLog(entry: AutomationLogEntry): Promise<void> {
100
+ try {
101
+ const dir = path.dirname(AUTOMATION_LOG_PATH);
102
+ await fs.mkdir(dir, { recursive: true });
103
+
104
+ const line = JSON.stringify(entry) + '\n';
105
+ await fs.appendFile(AUTOMATION_LOG_PATH, line);
106
+
107
+ // Check size and rotate if needed
108
+ try {
109
+ const stat = await fs.stat(AUTOMATION_LOG_PATH);
110
+ if (stat.size > AUTOMATION_LOG_MAX_BYTES) {
111
+ const content = await fs.readFile(AUTOMATION_LOG_PATH, 'utf-8');
112
+ const lines = content.trim().split('\n');
113
+ // Keep the newest half
114
+ const keep = lines.slice(Math.floor(lines.length / 2));
115
+ await fs.writeFile(AUTOMATION_LOG_PATH, keep.join('\n') + '\n');
116
+ }
117
+ } catch { /* rotation is best-effort */ }
118
+ } catch (err) {
119
+ console.error('[scheduler] Failed to write automation log:', err);
120
+ }
121
+ }
122
+
123
+ interface LivenessResult {
124
+ status: 'running' | 'stopped' | 'unknown';
125
+ exitCode?: number;
126
+ exitedAt?: string;
127
+ }
128
+
80
129
  /**
81
130
  * Check if a session is alive after startup (used for fresh sessions).
82
131
  *
@@ -84,17 +133,17 @@ async function fetchWithTimeout(url: string, opts?: RequestInit): Promise<Respon
84
133
  * don't need to verify prompt delivery. We just need to confirm the session
85
134
  * didn't crash during startup (e.g. auth failure, invalid config).
86
135
  */
87
- async function checkSessionAlive(sessionName: string): Promise<'running' | 'stopped' | 'unknown'> {
136
+ async function checkSessionAlive(sessionName: string): Promise<LivenessResult> {
88
137
  await new Promise(r => setTimeout(r, LIVENESS_CHECK_MS));
89
138
  try {
90
139
  const res = await fetchWithTimeout(`${BRIDGE_URL}/sessions/${encodeURIComponent(sessionName)}`);
91
- if (!res.ok) return 'unknown';
140
+ if (!res.ok) return { status: 'unknown' };
92
141
  const data = await res.json();
93
- if (data.status === 'stopped') return 'stopped';
94
- if (data.status === 'running' || data.status === 'detached') return 'running';
95
- return 'unknown';
142
+ if (data.status === 'stopped') return { status: 'stopped', exitCode: data.exitCode, exitedAt: data.exitedAt };
143
+ if (data.status === 'running' || data.status === 'detached') return { status: 'running' };
144
+ return { status: 'unknown' };
96
145
  } catch {
97
- return 'unknown';
146
+ return { status: 'unknown' };
98
147
  }
99
148
  }
100
149
 
@@ -357,6 +406,30 @@ export async function triggerAutomation(
357
406
  }
358
407
 
359
408
  const isFresh = config.freshSession || false;
409
+ const startTime = Date.now();
410
+
411
+ // Tracking for automation log
412
+ let bridgeRequestInfo: AutomationLogEntry['bridgeRequest'] = null;
413
+ let livenessInfo: AutomationLogEntry['livenessCheck'] = null;
414
+
415
+ const logAndReturn = async (result: KickoffResult): Promise<KickoffResult> => {
416
+ await writeAutomationLog({
417
+ timestamp: new Date().toISOString(),
418
+ cardId: card.id,
419
+ cardTitle: card.title,
420
+ projectId,
421
+ trigger: options.trigger,
422
+ provider,
423
+ sessionName,
424
+ fresh: isFresh,
425
+ bridgeRequest: bridgeRequestInfo,
426
+ livenessCheck: livenessInfo,
427
+ outcome: result.success ? 'success' : 'error',
428
+ error: result.error || null,
429
+ elapsedMs: Date.now() - startTime,
430
+ });
431
+ return result;
432
+ };
360
433
 
361
434
  try {
362
435
  console.log(`[scheduler] Creating session: ${sessionName} (fresh: ${isFresh}, provider: ${provider})`);
@@ -379,6 +452,7 @@ export async function triggerAutomation(
379
452
 
380
453
  if (!createRes.ok && createRes.status === 409 && !isFresh) {
381
454
  // Session exists and not fresh — try sending input directly (resume fallback)
455
+ bridgeRequestInfo = { status: 409 };
382
456
  console.log(`[scheduler] Session ${sessionName} returned 409, sending prompt via input endpoint`);
383
457
  const inputRes = await fetchWithTimeout(`${BRIDGE_URL}/sessions/${encodeURIComponent(sessionName)}/input`, {
384
458
  method: 'POST',
@@ -386,12 +460,22 @@ export async function triggerAutomation(
386
460
  body: JSON.stringify({ data: fullPrompt + '\r' }),
387
461
  });
388
462
  if (!inputRes.ok) {
389
- return { cardId: card.id, projectId, success: false, error: `Input failed: ${await inputRes.text()}` };
463
+ const body = await inputRes.text();
464
+ return logAndReturn({ cardId: card.id, projectId, success: false, error: `Input failed (${inputRes.status}): ${body}` });
390
465
  }
391
466
  } else if (!createRes.ok) {
392
- return { cardId: card.id, projectId, success: false, error: `Session create failed: ${await createRes.text()}` };
467
+ let errorDetail: string;
468
+ try {
469
+ const body = await createRes.json();
470
+ errorDetail = body.error || JSON.stringify(body);
471
+ } catch {
472
+ errorDetail = await createRes.text();
473
+ }
474
+ bridgeRequestInfo = { status: createRes.status, error: errorDetail };
475
+ return logAndReturn({ cardId: card.id, projectId, success: false, error: `Session create failed (${createRes.status}): ${errorDetail}` });
393
476
  } else {
394
477
  const createData = await createRes.json();
478
+ bridgeRequestInfo = { status: createRes.status, resumed: createData.resumed, pid: createData.pid };
395
479
  console.log(`[scheduler] Session created: ${sessionName} (status: ${createData.status}, resumed: ${createData.resumed}, pid: ${createData.pid})`);
396
480
  }
397
481
 
@@ -399,12 +483,20 @@ export async function triggerAutomation(
399
483
  // Prompt was delivered via CLI args. Just verify the session didn't crash.
400
484
  // No retry needed — OS guarantees prompt delivery.
401
485
  if (isFresh) {
402
- const status = await checkSessionAlive(sessionName);
403
- if (status === 'stopped') {
404
- return { cardId: card.id, projectId, success: false, sessionName, error: 'Session stopped during startup (possible auth/config error)' };
486
+ const liveness = await checkSessionAlive(sessionName);
487
+ livenessInfo = { type: 'checkSessionAlive', result: liveness.status, delayMs: LIVENESS_CHECK_MS, exitCode: liveness.exitCode, exitedAt: liveness.exitedAt };
488
+ if (liveness.status === 'stopped') {
489
+ // Exit code 0 means the session completed normally — not a failure.
490
+ // Fast automations can finish within the liveness check window.
491
+ if (liveness.exitCode === 0) {
492
+ return logAndReturn({ cardId: card.id, projectId, success: true, sessionName });
493
+ }
494
+ const exitDetail = liveness.exitCode !== undefined ? ` (exit code ${liveness.exitCode})` : '';
495
+ const aliveDetail = liveness.exitedAt ? `, alive ${((new Date(liveness.exitedAt).getTime() - startTime) / 1000).toFixed(1)}s` : '';
496
+ return logAndReturn({ cardId: card.id, projectId, success: false, sessionName, error: `Session stopped during startup${exitDetail}${aliveDetail}` });
405
497
  }
406
498
  // 'running' or 'unknown' — session is alive (or bridge is slow). Either way, prompt was delivered.
407
- return { cardId: card.id, projectId, success: true, sessionName };
499
+ return logAndReturn({ cardId: card.id, projectId, success: true, sessionName });
408
500
  }
409
501
 
410
502
  // --- Resume session path ---
@@ -412,7 +504,8 @@ export async function triggerAutomation(
412
504
  // so we check for activity and retry if needed.
413
505
  const active = await waitForActivity(sessionName);
414
506
  if (active) {
415
- return { cardId: card.id, projectId, success: true, sessionName };
507
+ livenessInfo = { type: 'waitForActivity', result: 'active' };
508
+ return logAndReturn({ cardId: card.id, projectId, success: true, sessionName });
416
509
  }
417
510
 
418
511
  // No activity detected — retry by re-sending prompt via bracketed paste
@@ -436,13 +529,15 @@ export async function triggerAutomation(
436
529
 
437
530
  const retryActive = await waitForActivity(sessionName);
438
531
  if (retryActive) {
439
- return { cardId: card.id, projectId, success: true, sessionName };
532
+ livenessInfo = { type: 'waitForActivity', result: 'active (retry)' };
533
+ return logAndReturn({ cardId: card.id, projectId, success: true, sessionName });
440
534
  }
441
535
  }
442
536
 
443
- return { cardId: card.id, projectId, success: false, sessionName, error: 'No activity detected after retry' };
537
+ livenessInfo = { type: 'waitForActivity', result: 'inactive after retry' };
538
+ return logAndReturn({ cardId: card.id, projectId, success: false, sessionName, error: 'No activity detected after retry' });
444
539
  } catch (err) {
445
- return { cardId: card.id, projectId, success: false, sessionName, error: (err as Error).message };
540
+ return logAndReturn({ cardId: card.id, projectId, success: false, sessionName, error: (err as Error).message });
446
541
  }
447
542
  }
448
543
 
@@ -478,12 +573,17 @@ export async function updateCardAutomation(
478
573
  }
479
574
 
480
575
  /**
481
- * Send error notification via messaging
576
+ * Send error notification via messaging with actionable detail.
482
577
  */
483
- async function sendErrorNotification(cardTitle: string, error: string): Promise<void> {
578
+ async function sendErrorNotification(cardTitle: string, error: string, sessionName?: string): Promise<void> {
484
579
  try {
485
580
  const { execSync } = await import('child_process');
486
- execSync(`sly-messaging send "Automation kickoff failed: ${cardTitle.replace(/"/g, '\\"')} — ${error.replace(/"/g, '\\"')}"`, {
581
+ const lines = [`Automation failed: ${cardTitle}`];
582
+ if (sessionName) lines.push(`Session: ${sessionName}`);
583
+ lines.push(`Error: ${error}`);
584
+ lines.push(`Log: ~/.slycode/logs/automation.log`);
585
+ const msg = lines.join('\n').replace(/"/g, '\\"');
586
+ execSync(`sly-messaging send "${msg}"`, {
487
587
  timeout: 10_000,
488
588
  stdio: 'pipe',
489
589
  });
@@ -560,7 +660,7 @@ async function checkAutomations(): Promise<void> {
560
660
  result.error.includes('No automation config')
561
661
  );
562
662
  if (isHardFailure) {
563
- await sendErrorNotification(card.title, result.error || 'Unknown error');
663
+ await sendErrorNotification(card.title, result.error || 'Unknown error', result.sessionName);
564
664
  } else {
565
665
  console.log(`[scheduler] Soft failure for ${card.id}, skipping notification: ${result.error}`);
566
666
  }