cueclaw 0.1.2 → 0.1.3

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  cueclawHome
3
- } from "./chunk-RSKXBXSJ.js";
3
+ } from "./chunk-25KI643G.js";
4
4
 
5
5
  // src/db.ts
6
6
  import Database from "better-sqlite3";
@@ -113,6 +113,24 @@ function insertWorkflow(db, workflow) {
113
113
  workflow.updated_at
114
114
  );
115
115
  }
116
+ function upsertWorkflow(db, workflow) {
117
+ db.prepare(`
118
+ INSERT OR REPLACE INTO workflows (id, name, description, trigger_json, steps_json, failure_policy_json, phase, schema_version, metadata_json, created_at, updated_at)
119
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
120
+ `).run(
121
+ workflow.id,
122
+ workflow.name,
123
+ workflow.description,
124
+ JSON.stringify(workflow.trigger),
125
+ JSON.stringify(workflow.steps),
126
+ JSON.stringify(workflow.failure_policy),
127
+ workflow.phase,
128
+ workflow.schema_version,
129
+ workflow.metadata ? JSON.stringify(workflow.metadata) : null,
130
+ workflow.created_at,
131
+ workflow.updated_at
132
+ );
133
+ }
116
134
  function getWorkflow(db, id) {
117
135
  const row = db.prepare("SELECT * FROM workflows WHERE id = ?").get(id);
118
136
  return row ? rowToWorkflow(row) : void 0;
@@ -171,6 +189,7 @@ function getWorkflowRunsByWorkflowId(db, workflowId) {
171
189
  export {
172
190
  initDb,
173
191
  insertWorkflow,
192
+ upsertWorkflow,
174
193
  getWorkflow,
175
194
  listWorkflows,
176
195
  updateWorkflowPhase,
@@ -0,0 +1,116 @@
1
+ // src/logger.ts
2
+ import pino from "pino";
3
+ import { PassThrough } from "stream";
4
+ import { createWriteStream, mkdirSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ var listeners = /* @__PURE__ */ new Set();
8
+ var tuiStream = new PassThrough();
9
+ tuiStream.on("data", (chunk) => {
10
+ for (const raw of chunk.toString().split("\n")) {
11
+ if (!raw.trim()) continue;
12
+ try {
13
+ const obj = JSON.parse(raw);
14
+ const lvl = (pino.levels.labels[obj.level] ?? "INFO").toUpperCase();
15
+ const mod = obj.module ? ` [${obj.module}]` : "";
16
+ const msg = obj.msg ?? "";
17
+ for (const fn of listeners) fn(`${lvl}${mod} ${msg}`);
18
+ } catch {
19
+ for (const fn of listeners) fn(raw);
20
+ }
21
+ }
22
+ });
23
+ var fileStream = null;
24
+ var logDir = null;
25
+ var configuredLevel = null;
26
+ function resolveDir(dir) {
27
+ return dir.startsWith("~") ? join(homedir(), dir.slice(1)) : dir;
28
+ }
29
+ function getLevel() {
30
+ return configuredLevel ?? process.env["LOG_LEVEL"] ?? "info";
31
+ }
32
+ var logger = pino({
33
+ level: process.env["LOG_LEVEL"] ?? "info",
34
+ transport: process.env["NODE_ENV"] === "production" ? void 0 : { target: "pino-pretty", options: { colorize: true } }
35
+ });
36
+ function initLogger(opts) {
37
+ const level = opts.level ?? process.env["LOG_LEVEL"] ?? "info";
38
+ configuredLevel = level;
39
+ if (opts.dir) {
40
+ const resolved = resolveDir(opts.dir);
41
+ logDir = resolved;
42
+ mkdirSync(join(resolved, "executions"), { recursive: true });
43
+ fileStream = createWriteStream(join(resolved, "daemon.log"), { flags: "a" });
44
+ logger = pino(
45
+ { level },
46
+ pino.multistream([
47
+ { stream: process.stdout },
48
+ { stream: fileStream }
49
+ ])
50
+ );
51
+ } else {
52
+ logger = pino({
53
+ level,
54
+ transport: process.env["NODE_ENV"] === "production" ? void 0 : { target: "pino-pretty", options: { colorize: true } }
55
+ });
56
+ }
57
+ }
58
+ function enableTuiLogging() {
59
+ if (fileStream) {
60
+ logger = pino(
61
+ { level: getLevel() },
62
+ pino.multistream([
63
+ { stream: tuiStream },
64
+ { stream: fileStream }
65
+ ])
66
+ );
67
+ } else {
68
+ logger = pino({ level: getLevel() }, tuiStream);
69
+ }
70
+ }
71
+ function onLogLine(fn) {
72
+ listeners.add(fn);
73
+ return () => {
74
+ listeners.delete(fn);
75
+ };
76
+ }
77
+ function createChildLogger(bindings) {
78
+ return logger.child(bindings);
79
+ }
80
+ function createExecutionLogger(workflowId, runId) {
81
+ if (!logDir) {
82
+ return logger.child({ workflowId, runId });
83
+ }
84
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
85
+ const filename = `${workflowId}_${date}.log`;
86
+ const execStream = createWriteStream(join(logDir, "executions", filename), { flags: "a" });
87
+ return pino(
88
+ { level: getLevel() },
89
+ pino.multistream([
90
+ { stream: execStream },
91
+ ...fileStream ? [{ stream: fileStream }] : []
92
+ ])
93
+ );
94
+ }
95
+ function resetLogger() {
96
+ if (fileStream) {
97
+ fileStream.end();
98
+ fileStream = null;
99
+ }
100
+ logDir = null;
101
+ configuredLevel = null;
102
+ logger = pino({
103
+ level: process.env["LOG_LEVEL"] ?? "info",
104
+ transport: process.env["NODE_ENV"] === "production" ? void 0 : { target: "pino-pretty", options: { colorize: true } }
105
+ });
106
+ }
107
+
108
+ export {
109
+ logger,
110
+ initLogger,
111
+ enableTuiLogging,
112
+ onLogLine,
113
+ createChildLogger,
114
+ createExecutionLogger,
115
+ resetLogger
116
+ };
@@ -4,18 +4,23 @@ import {
4
4
  updateStepRunStatus,
5
5
  updateWorkflowPhase,
6
6
  updateWorkflowRunStatus
7
- } from "./chunk-G43R5ASK.js";
7
+ } from "./chunk-JJUF2AJ5.js";
8
8
  import {
9
9
  cueclawHome,
10
+ getDefaultImage,
10
11
  loadConfig
11
- } from "./chunk-RSKXBXSJ.js";
12
+ } from "./chunk-25KI643G.js";
12
13
  import {
13
14
  ConfigError,
14
15
  ExecutorError
15
16
  } from "./chunk-BVQG3WYO.js";
16
17
  import {
18
+ isDev
19
+ } from "./chunk-ZCK3IFLC.js";
20
+ import {
21
+ createExecutionLogger,
17
22
  logger
18
- } from "./chunk-QBOYMF4A.js";
23
+ } from "./chunk-KBLMQZ3P.js";
19
24
 
20
25
  // src/executor.ts
21
26
  import { nanoid as nanoid3 } from "nanoid";
@@ -239,7 +244,7 @@ async function runContainerAgent(opts) {
239
244
  mkdirSync2(opts.workDir, { recursive: true });
240
245
  mkdirSync2(join3(opts.ipcDir, "input"), { recursive: true });
241
246
  mkdirSync2(join3(opts.ipcDir, "output"), { recursive: true });
242
- const image = config.container?.image ?? "cueclaw-agent:latest";
247
+ const image = config.container?.image ?? getDefaultImage();
243
248
  const network = config.container?.network ?? "none";
244
249
  const volumeMounts = buildVolumeMounts(opts);
245
250
  const dockerArgs = [
@@ -292,7 +297,8 @@ function buildVolumeMounts(opts) {
292
297
  return mounts;
293
298
  }
294
299
  async function spawnContainer(dockerArgs, opts, config) {
295
- return new Promise((resolve) => {
300
+ return new Promise((resolve2) => {
301
+ logger.info({ containerName: opts.containerName, image: dockerArgs[dockerArgs.length - 1] }, "Starting container");
296
302
  const proc = spawn("docker", dockerArgs, {
297
303
  stdio: ["pipe", "pipe", "pipe"]
298
304
  });
@@ -348,8 +354,9 @@ async function spawnContainer(dockerArgs, opts, config) {
348
354
  proc.on("close", (code) => {
349
355
  clearTimeout(hardTimer);
350
356
  clearInterval(idleCheck);
357
+ logger.info({ containerName: opts.containerName, exitCode: code }, "Container exited");
351
358
  if (truncated) {
352
- resolve({ status: "failed", error: "Container output size cap exceeded" });
359
+ resolve2({ status: "failed", error: "Container output size cap exceeded" });
353
360
  return;
354
361
  }
355
362
  const startIdx = stdout.indexOf(OUTPUT_START_MARKER);
@@ -358,7 +365,7 @@ async function spawnContainer(dockerArgs, opts, config) {
358
365
  resultBuffer = stdout.slice(startIdx + OUTPUT_START_MARKER.length + 1, endIdx).trim();
359
366
  }
360
367
  if (code !== 0 && !resultBuffer) {
361
- resolve({
368
+ resolve2({
362
369
  status: "failed",
363
370
  error: stderr || `Container exited with code ${code}`
364
371
  });
@@ -366,16 +373,16 @@ async function spawnContainer(dockerArgs, opts, config) {
366
373
  }
367
374
  try {
368
375
  const parsed = JSON.parse(resultBuffer);
369
- resolve({
376
+ resolve2({
370
377
  status: "succeeded",
371
378
  output: parsed.result ?? null,
372
379
  sessionId: parsed.sessionId
373
380
  });
374
381
  } catch {
375
382
  if (resultBuffer) {
376
- resolve({ status: "succeeded", output: resultBuffer });
383
+ resolve2({ status: "succeeded", output: resultBuffer });
377
384
  } else {
378
- resolve({
385
+ resolve2({
379
386
  status: "failed",
380
387
  error: stderr || "No output captured from container"
381
388
  });
@@ -385,7 +392,8 @@ async function spawnContainer(dockerArgs, opts, config) {
385
392
  proc.on("error", (err) => {
386
393
  clearTimeout(hardTimer);
387
394
  clearInterval(idleCheck);
388
- resolve({
395
+ logger.error({ containerName: opts.containerName, err }, "Docker spawn error");
396
+ resolve2({
389
397
  status: "failed",
390
398
  error: `Docker spawn error: ${err.message}`
391
399
  });
@@ -393,6 +401,7 @@ async function spawnContainer(dockerArgs, opts, config) {
393
401
  });
394
402
  }
395
403
  function gracefulStop(containerName) {
404
+ logger.debug({ containerName }, "Gracefully stopping container");
396
405
  try {
397
406
  spawn("docker", ["stop", containerName]);
398
407
  } catch {
@@ -418,35 +427,113 @@ function prepareContainerOpts(workflowId, stepId, runId, prompt, cwd, allowedToo
418
427
  };
419
428
  }
420
429
 
430
+ // src/container-runtime.ts
431
+ import { execFileSync } from "child_process";
432
+ import { resolve, dirname } from "path";
433
+ import { fileURLToPath } from "url";
434
+ import { existsSync as existsSync3 } from "fs";
435
+ var cached;
436
+ var imageCache = /* @__PURE__ */ new Map();
437
+ function isDockerAvailable() {
438
+ if (cached !== void 0) return cached;
439
+ try {
440
+ execFileSync("docker", ["info"], { encoding: "utf-8", stdio: "pipe" });
441
+ cached = true;
442
+ } catch {
443
+ cached = false;
444
+ }
445
+ return cached;
446
+ }
447
+ function isDockerImageAvailable(image) {
448
+ const hit = imageCache.get(image);
449
+ if (hit !== void 0) return hit;
450
+ try {
451
+ execFileSync("docker", ["image", "inspect", image], { encoding: "utf-8", stdio: "pipe" });
452
+ imageCache.set(image, true);
453
+ return true;
454
+ } catch {
455
+ imageCache.set(image, false);
456
+ return false;
457
+ }
458
+ }
459
+ function ensureDockerImage(image) {
460
+ if (isDockerImageAvailable(image)) return true;
461
+ if (isDev) {
462
+ return buildDevImage(image);
463
+ }
464
+ logger.info({ image }, "Docker image not found locally, attempting pull");
465
+ try {
466
+ execFileSync("docker", ["pull", image], { encoding: "utf-8", stdio: "pipe", timeout: 3e5 });
467
+ imageCache.set(image, true);
468
+ logger.info({ image }, "Docker image pulled successfully");
469
+ return true;
470
+ } catch (err) {
471
+ logger.warn({ image, err }, "Failed to pull Docker image");
472
+ return false;
473
+ }
474
+ }
475
+ function buildDevImage(image) {
476
+ const thisDir = dirname(fileURLToPath(import.meta.url));
477
+ const projectRoot = resolve(thisDir, "..");
478
+ const buildScript = resolve(projectRoot, "container", "build.sh");
479
+ if (!existsSync3(buildScript)) {
480
+ logger.warn({ buildScript }, "container/build.sh not found, cannot auto-build");
481
+ return false;
482
+ }
483
+ logger.info({ image }, "Dev mode: auto-building container image via container/build.sh");
484
+ try {
485
+ execFileSync("bash", [buildScript], {
486
+ encoding: "utf-8",
487
+ stdio: "inherit",
488
+ cwd: resolve(projectRoot, "container")
489
+ });
490
+ imageCache.set(image, true);
491
+ logger.info({ image }, "Dev mode: container image built successfully");
492
+ return true;
493
+ } catch (err) {
494
+ logger.warn({ image, err }, "Dev mode: container image build failed");
495
+ return false;
496
+ }
497
+ }
498
+
421
499
  // src/agent-runner.ts
422
500
  function runAgent(opts) {
423
501
  const config = loadConfig();
424
- if (config.container?.enabled && opts.workflowId && opts.stepId && opts.runId) {
425
- const containerOpts = prepareContainerOpts(
426
- opts.workflowId,
427
- opts.stepId,
428
- opts.runId,
429
- opts.prompt,
430
- opts.cwd,
431
- opts.allowedTools
432
- );
433
- const resultPromise2 = runContainerAgent({
434
- ...containerOpts,
435
- signal: opts.signal,
436
- onProgress: opts.onProgress
437
- });
438
- return {
439
- resultPromise: resultPromise2,
440
- abort: () => {
441
- try {
442
- import("child_process").then(({ spawn: spawn2 }) => {
443
- spawn2("docker", ["stop", containerOpts.containerName]);
444
- });
445
- } catch {
502
+ const containerEnabled = config.container?.enabled ?? false;
503
+ if (containerEnabled && opts.workflowId && opts.stepId && opts.runId) {
504
+ if (!isDockerAvailable()) {
505
+ logger.warn({ stepId: opts.stepId }, "Docker not available, falling back to local execution");
506
+ } else if (!ensureDockerImage(config.container?.image ?? getDefaultImage())) {
507
+ logger.warn({ stepId: opts.stepId, image: config.container?.image ?? getDefaultImage() }, "Docker image not available (pull failed), falling back to local execution");
508
+ } else {
509
+ logger.info({ stepId: opts.stepId, mode: "container" }, "Running agent in container mode");
510
+ const containerOpts = prepareContainerOpts(
511
+ opts.workflowId,
512
+ opts.stepId,
513
+ opts.runId,
514
+ opts.prompt,
515
+ opts.cwd,
516
+ opts.allowedTools
517
+ );
518
+ const resultPromise2 = runContainerAgent({
519
+ ...containerOpts,
520
+ signal: opts.signal,
521
+ onProgress: opts.onProgress
522
+ });
523
+ return {
524
+ resultPromise: resultPromise2,
525
+ abort: () => {
526
+ try {
527
+ import("child_process").then(({ spawn: spawn2 }) => {
528
+ spawn2("docker", ["stop", containerOpts.containerName]);
529
+ });
530
+ } catch {
531
+ }
446
532
  }
447
- }
448
- };
533
+ };
534
+ }
449
535
  }
536
+ logger.info({ stepId: opts.stepId, mode: "local" }, "Running agent in local mode");
450
537
  let aborted = false;
451
538
  const resultPromise = (async () => {
452
539
  const authToken = config.claude.executor.api_key ?? config.claude.api_key;
@@ -511,6 +598,7 @@ function runAgent(opts) {
511
598
  }
512
599
  opts.onProgress?.(message);
513
600
  }
601
+ logger.debug({ stepId: opts.stepId, status: "succeeded" }, "Agent completed");
514
602
  return { status: "succeeded", output: result, sessionId };
515
603
  } catch (err) {
516
604
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -547,13 +635,16 @@ function createSession(db, stepRunId, sdkSessionId) {
547
635
  INSERT INTO sessions (id, step_run_id, sdk_session_id, created_at, last_used_at, is_active)
548
636
  VALUES (?, ?, ?, ?, ?, ?)
549
637
  `).run(session.id, session.step_run_id, session.sdk_session_id ?? null, session.created_at, session.last_used_at, 1);
638
+ logger.debug({ sessionId: session.id, stepRunId }, "Session created");
550
639
  return session;
551
640
  }
552
641
  function deactivateSession(db, sessionId) {
553
642
  db.prepare("UPDATE sessions SET is_active = 0, last_used_at = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), sessionId);
643
+ logger.debug({ sessionId }, "Session deactivated");
554
644
  }
555
645
  function updateSessionSdkId(db, sessionId, sdkSessionId) {
556
646
  db.prepare("UPDATE sessions SET sdk_session_id = ?, last_used_at = ? WHERE id = ?").run(sdkSessionId, (/* @__PURE__ */ new Date()).toISOString(), sessionId);
647
+ logger.debug({ sessionId, sdkSessionId }, "Session SDK ID updated");
557
648
  }
558
649
 
559
650
  // src/executor.ts
@@ -599,7 +690,7 @@ function hasSkipMarker(inputs) {
599
690
  }
600
691
  return false;
601
692
  }
602
- async function executeStepOnce(step, resolvedInputs, runId, db, cwd, onProgress) {
693
+ async function executeStepOnce(step, resolvedInputs, runId, db, cwd, onProgress, execLogger) {
603
694
  const stepRunId = `sr_${nanoid3()}`;
604
695
  const now = (/* @__PURE__ */ new Date()).toISOString();
605
696
  const stepRun = {
@@ -610,6 +701,7 @@ async function executeStepOnce(step, resolvedInputs, runId, db, cwd, onProgress)
610
701
  started_at: now
611
702
  };
612
703
  insertStepRun(db, stepRun);
704
+ execLogger?.info({ stepId: step.id, stepRunId, attempt: "start" }, `Step started: ${step.id}`);
613
705
  const inputContext = Object.keys(resolvedInputs).length > 0 ? `
614
706
 
615
707
  Inputs:
@@ -628,15 +720,21 @@ ${JSON.stringify(resolvedInputs, null, 2)}` : "";
628
720
  const session = createSession(db, stepRunId, result.sessionId);
629
721
  updateSessionSdkId(db, session.id, result.sessionId);
630
722
  deactivateSession(db, session.id);
723
+ execLogger?.debug({ stepId: step.id, sessionId: session.id }, "Session stored");
631
724
  }
632
725
  updateStepRunStatus(db, stepRunId, result.status, result.output ?? void 0, result.error);
726
+ if (result.status === "failed") {
727
+ execLogger?.error({ stepId: step.id, stepRunId, error: result.error }, `Step failed: ${step.id}`);
728
+ } else {
729
+ execLogger?.info({ stepId: step.id, stepRunId, status: result.status }, `Step completed: ${step.id}`);
730
+ }
633
731
  return result;
634
732
  }
635
- async function executeStepWithRetry(step, resolvedInputs, runId, db, cwd, policy, onProgress) {
733
+ async function executeStepWithRetry(step, resolvedInputs, runId, db, cwd, policy, onProgress, execLogger) {
636
734
  const maxRetries = policy.max_retries ?? 0;
637
735
  let delay = policy.retry_delay_ms ?? 5e3;
638
736
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
639
- const result = await executeStepOnce(step, resolvedInputs, runId, db, cwd, onProgress);
737
+ const result = await executeStepOnce(step, resolvedInputs, runId, db, cwd, onProgress, execLogger);
640
738
  if (result.status !== "failed" || attempt === maxRetries) return result;
641
739
  logger.info({ stepId: step.id, attempt, delay }, "Retrying step");
642
740
  await new Promise((r) => setTimeout(r, delay));
@@ -656,6 +754,8 @@ async function executeWorkflow(opts) {
656
754
  };
657
755
  insertWorkflowRun(db, run);
658
756
  updateWorkflowPhase(db, workflow.id, "executing");
757
+ const execLogger = createExecutionLogger(workflow.id, runId);
758
+ execLogger.info({ workflowId: workflow.id, runId, steps: workflow.steps.length }, "Workflow execution started");
659
759
  const completed = /* @__PURE__ */ new Map();
660
760
  const remaining = new Set(workflow.steps.map((s) => s.id));
661
761
  const stepMap = new Map(workflow.steps.map((s) => [s.id, s]));
@@ -663,6 +763,7 @@ async function executeWorkflow(opts) {
663
763
  try {
664
764
  while (remaining.size > 0) {
665
765
  if (signal?.aborted) {
766
+ execLogger.warn({ workflowId: workflow.id, runId, remainingSteps: remaining.size }, "Execution aborted via signal");
666
767
  for (const id of remaining) {
667
768
  completed.set(id, { status: "skipped", error: "Aborted" });
668
769
  onProgress?.(id, { status: "skipped" });
@@ -676,6 +777,7 @@ async function executeWorkflow(opts) {
676
777
  return step.depends_on.every((dep) => completed.has(dep));
677
778
  });
678
779
  if (ready.length === 0) {
780
+ execLogger.error({ workflowId: workflow.id, runId, remaining: [...remaining] }, "Deadlock detected");
679
781
  throw new ExecutorError("Deadlock: no ready steps but remaining steps exist");
680
782
  }
681
783
  const executable = [];
@@ -685,6 +787,7 @@ async function executeWorkflow(opts) {
685
787
  (dep) => completed.get(dep)?.status === "failed" || completed.get(dep)?.status === "skipped"
686
788
  );
687
789
  if (depsFailed && workflow.failure_policy.on_step_failure !== "ask_user") {
790
+ execLogger.debug({ stepId: id, reason: "dependency_failed" }, "Step skipped");
688
791
  remaining.delete(id);
689
792
  completed.set(id, { status: "skipped" });
690
793
  const skipRunId = `sr_${nanoid3()}`;
@@ -693,6 +796,7 @@ async function executeWorkflow(opts) {
693
796
  }
694
797
  const resolvedInputs = resolveInputs(step.inputs, completed, triggerData);
695
798
  if (hasSkipMarker(resolvedInputs)) {
799
+ execLogger.debug({ stepId: id, reason: "skip_marker" }, "Step skipped");
696
800
  remaining.delete(id);
697
801
  completed.set(id, { status: "skipped" });
698
802
  const skipRunId = `sr_${nanoid3()}`;
@@ -713,7 +817,8 @@ async function executeWorkflow(opts) {
713
817
  db,
714
818
  cwd,
715
819
  workflow.failure_policy,
716
- onProgress
820
+ onProgress,
821
+ execLogger
717
822
  );
718
823
  return { stepId: step.id, result };
719
824
  })
@@ -723,6 +828,7 @@ async function executeWorkflow(opts) {
723
828
  if (result.status === "failed") {
724
829
  const policy = workflow.failure_policy.on_step_failure;
725
830
  if (policy === "stop") {
831
+ execLogger.warn({ stepId, policy: "stop" }, "Stop policy triggered, skipping remaining steps");
726
832
  for (const remainingId of remaining) {
727
833
  completed.set(remainingId, { status: "skipped" });
728
834
  const skipRunId = `sr_${nanoid3()}`;
@@ -734,6 +840,7 @@ async function executeWorkflow(opts) {
734
840
  }
735
841
  if (policy === "ask_user" && onStepFailure) {
736
842
  const decision = await onStepFailure(stepMap.get(stepId), result.error ?? "Unknown error");
843
+ execLogger.info({ stepId, decision }, "ask_user decision received");
737
844
  if (decision === "stop") {
738
845
  for (const remainingId of remaining) {
739
846
  completed.set(remainingId, { status: "skipped" });
@@ -759,6 +866,7 @@ async function executeWorkflow(opts) {
759
866
  } else {
760
867
  updateWorkflowPhase(db, workflow.id, "active");
761
868
  }
869
+ execLogger.info({ workflowId: workflow.id, runId, status: finalStatus }, "Workflow execution finished");
762
870
  return { runId, status: finalStatus, results: completed };
763
871
  }
764
872
 
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  createAnthropicClient
3
3
  } from "./chunk-DVQFSFIZ.js";
4
- import {
5
- getConfiguredSecretKeys
6
- } from "./chunk-ZCK3IFLC.js";
7
4
  import {
8
5
  PlannerError
9
6
  } from "./chunk-BVQG3WYO.js";
7
+ import {
8
+ getConfiguredSecretKeys
9
+ } from "./chunk-ZCK3IFLC.js";
10
10
  import {
11
11
  logger
12
- } from "./chunk-QBOYMF4A.js";
12
+ } from "./chunk-KBLMQZ3P.js";
13
13
 
14
14
  // src/planner.ts
15
15
  import { z } from "zod/v4";
@@ -236,9 +236,26 @@ function parsePlannerToolResponse(response) {
236
236
  }
237
237
  return { type: "error", error: "Unexpected planner response format" };
238
238
  }
239
- function buildPlannerSystemPrompt(config) {
239
+ function buildPlannerSystemPrompt(config, channelContext) {
240
240
  const identity = config.identity?.name ? `
241
241
  User identity: ${config.identity.name}` : "";
242
+ let channelSection;
243
+ if (channelContext && channelContext.channel !== "tui") {
244
+ channelSection = `
245
+ ## Channel Context
246
+
247
+ The user is requesting this workflow via **${channelContext.channel}**.
248
+ - Chat ID: ${channelContext.chatJid}
249
+ - Sender: ${channelContext.sender}
250
+
251
+ When the workflow needs to send notifications or messages back to the user, use the chat ID and channel above as the recipient. Do not ask the user for recipient information \u2014 you already have it.`;
252
+ } else {
253
+ channelSection = `
254
+ ## Channel Context
255
+
256
+ The user is using the TUI (terminal interface). No chat recipient is available.
257
+ If the workflow needs to send notifications or messages to someone, the step description must require explicit recipient input (e.g., email address, phone number, chat ID) \u2014 do not assume any default recipient.`;
258
+ }
242
259
  return `You are CueClaw Planner. Convert user's natural language into a structured Workflow.
243
260
 
244
261
  ## Execution Environment
@@ -276,9 +293,9 @@ You can reference these in workflow steps \u2014 they are available as environme
276
293
  If a workflow needs credentials not listed above, use the set_secret tool to store them after the user provides the value. Never invent or guess secret values.
277
294
 
278
295
  ## User Identity
279
- ${identity}`;
296
+ ${identity}${channelSection}`;
280
297
  }
281
- async function generatePlan(userDescription, config) {
298
+ async function generatePlan(userDescription, config, channelContext) {
282
299
  const anthropic = createAnthropicClient(config);
283
300
  const MAX_RETRIES = 2;
284
301
  let retryContext = "";
@@ -292,13 +309,14 @@ ${retryContext}` : userDescription;
292
309
  response = await anthropic.messages.create({
293
310
  model: config.claude.planner.model,
294
311
  max_tokens: 4096,
295
- system: buildPlannerSystemPrompt(config),
312
+ system: buildPlannerSystemPrompt(config, channelContext),
296
313
  messages: [{ role: "user", content: prompt }],
297
314
  tools: [plannerTool],
298
315
  tool_choice: { type: "tool", name: "create_workflow" }
299
316
  });
300
317
  } catch (err) {
301
318
  const detail = err instanceof Error ? err.message : String(err);
319
+ logger.error({ err, attempt }, "Planner API request failed");
302
320
  throw new PlannerError(
303
321
  `API request failed: ${detail}. Check your API key and base_url in ~/.cueclaw/config.yaml`
304
322
  );
@@ -322,6 +340,7 @@ ${retryContext}` : userDescription;
322
340
  if (!parseResult.success) {
323
341
  const errMsg = parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
324
342
  if (attempt < MAX_RETRIES) {
343
+ logger.warn({ attempt, errors: errMsg }, "Planner output validation failed, retrying");
325
344
  retryContext = `[System] Previous plan had validation issues:
326
345
  ${errMsg}
327
346
  Please fix and try again.`;
@@ -332,6 +351,7 @@ Please fix and try again.`;
332
351
  const dagErrors = validateDAG(parseResult.data.steps);
333
352
  if (dagErrors.length > 0) {
334
353
  if (attempt < MAX_RETRIES) {
354
+ logger.warn({ attempt, dagErrors }, "DAG validation failed, retrying");
335
355
  retryContext = `[System] DAG dependency issues:
336
356
  ${dagErrors.join("\n")}
337
357
  Please fix the step dependencies.`;
@@ -340,7 +360,7 @@ Please fix the step dependencies.`;
340
360
  throw new PlannerError(`DAG validation failed after ${MAX_RETRIES + 1} attempts: ${dagErrors.join(", ")}`);
341
361
  }
342
362
  const now = (/* @__PURE__ */ new Date()).toISOString();
343
- return {
363
+ const workflow = {
344
364
  ...parseResult.data,
345
365
  schema_version: "1.0",
346
366
  id: `wf_${nanoid()}`,
@@ -348,10 +368,12 @@ Please fix the step dependencies.`;
348
368
  created_at: now,
349
369
  updated_at: now
350
370
  };
371
+ logger.info({ name: workflow.name, stepCount: workflow.steps.length, workflowId: workflow.id }, "Plan generated successfully");
372
+ return workflow;
351
373
  }
352
374
  throw new PlannerError("Failed to generate valid plan after retries");
353
375
  }
354
- async function modifyPlan(originalWorkflow, modificationDescription, config) {
376
+ async function modifyPlan(originalWorkflow, modificationDescription, config, channelContext) {
355
377
  const plannerOutput = {
356
378
  name: originalWorkflow.name,
357
379
  description: originalWorkflow.description,
@@ -369,7 +391,8 @@ ${modificationDescription}
369
391
 
370
392
  Preserve unmodified steps' IDs, descriptions, and dependencies \u2014 only change what the user specified.
371
393
  Return the complete modified workflow using the create_workflow tool.`;
372
- const result = await generatePlan(combinedPrompt, config);
394
+ const result = await generatePlan(combinedPrompt, config, channelContext);
395
+ logger.info({ workflowId: originalWorkflow.id }, "Plan modified");
373
396
  return {
374
397
  ...result,
375
398
  id: originalWorkflow.id,
@@ -381,6 +404,7 @@ function confirmPlan(workflow) {
381
404
  throw new PlannerError(`Cannot confirm workflow in phase "${workflow.phase}"`);
382
405
  }
383
406
  const nextPhase = workflow.trigger.type === "manual" ? "executing" : "active";
407
+ logger.info({ workflowId: workflow.id, nextPhase }, "Plan confirmed");
384
408
  return {
385
409
  ...workflow,
386
410
  phase: nextPhase,
@@ -388,6 +412,7 @@ function confirmPlan(workflow) {
388
412
  };
389
413
  }
390
414
  function rejectPlan(workflow) {
415
+ logger.info({ workflowId: workflow.id }, "Plan rejected");
391
416
  return {
392
417
  ...workflow,
393
418
  phase: "planning",