@rk0429/agentic-relay 21.0.0 → 21.1.2

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 (35) hide show
  1. package/dist/application/projection-service.js +3 -0
  2. package/dist/application/projection-service.js.map +1 -1
  3. package/dist/application/routine-catalog-service.d.ts +1 -0
  4. package/dist/application/routine-catalog-service.js +15 -1
  5. package/dist/application/routine-catalog-service.js.map +1 -1
  6. package/dist/application/routine-daemon-service.d.ts +9 -0
  7. package/dist/application/routine-daemon-service.js +67 -1
  8. package/dist/application/routine-daemon-service.js.map +1 -1
  9. package/dist/application/routine-event-log-sink.js +1 -1
  10. package/dist/application/routine-event-log-sink.js.map +1 -1
  11. package/dist/application/routine-run-service.d.ts +5 -1
  12. package/dist/application/routine-run-service.js +26 -4
  13. package/dist/application/routine-run-service.js.map +1 -1
  14. package/dist/application/routine-status-query-service.d.ts +34 -9
  15. package/dist/application/routine-status-query-service.js +115 -43
  16. package/dist/application/routine-status-query-service.js.map +1 -1
  17. package/dist/bin/relay.d.ts +4 -2
  18. package/dist/bin/relay.js +171 -18
  19. package/dist/bin/relay.js.map +1 -1
  20. package/dist/core/types.d.ts +1 -1
  21. package/dist/domain/routine-name.js +3 -3
  22. package/dist/domain/routine-name.js.map +1 -1
  23. package/dist/domain/routine-schedule.d.ts +5 -1
  24. package/dist/domain/routine-schedule.js +20 -4
  25. package/dist/domain/routine-schedule.js.map +1 -1
  26. package/dist/domain/trigger-config.js +2 -2
  27. package/dist/domain/trigger-config.js.map +1 -1
  28. package/dist/infrastructure/store/routine-runtime-projection-repository.d.ts +2 -0
  29. package/dist/infrastructure/store/routine-runtime-projection-repository.js +16 -0
  30. package/dist/infrastructure/store/routine-runtime-projection-repository.js.map +1 -1
  31. package/dist/interfaces/cli/relay-cli-args.d.ts +1 -0
  32. package/dist/interfaces/cli/relay-cli-args.js +13 -0
  33. package/dist/interfaces/cli/relay-cli-args.js.map +1 -1
  34. package/dist/interfaces/mcp/relay-mcp-server.js +1 -1
  35. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { ValidationError } from "../core/errors.js";
2
2
  import { isProcessAlive } from "./housekeeping.js";
3
3
  import { RoutineName } from "../domain/routine-name.js";
4
+ const STALE_HEARTBEAT_THRESHOLD_MS = 60_000;
4
5
  export class RoutineStatusQueryService {
5
6
  deps;
6
7
  checkProcessAlive;
@@ -10,17 +11,24 @@ export class RoutineStatusQueryService {
10
11
  }
11
12
  async list() {
12
13
  const merged = await this.buildMergedView();
13
- return merged.map((entry) => ({
14
- name: entry.name,
15
- status: entry.status,
16
- nextScheduledAt: entry.nextScheduledAt,
17
- lastRun: entry.lastRun,
18
- }));
14
+ return {
15
+ entries: merged.entries.map((entry) => ({
16
+ name: entry.name,
17
+ status: entry.status,
18
+ schedule: entry.schedule,
19
+ missedPolicy: entry.missedPolicy,
20
+ nextScheduledAt: entry.nextScheduledAt,
21
+ lastRun: entry.lastRun,
22
+ lastScheduledAt: entry.lastScheduledAt,
23
+ warnings: entry.warnings,
24
+ })),
25
+ invalid: merged.invalid,
26
+ };
19
27
  }
20
28
  async status(name, options = {}) {
21
29
  const routineName = name instanceof RoutineName ? name.value : name;
22
30
  const merged = await this.buildMergedView(options.limit ?? 10);
23
- const entry = merged.find((candidate) => candidate.name === routineName);
31
+ const entry = merged.entries.find((candidate) => candidate.name === routineName);
24
32
  if (!entry) {
25
33
  throw new ValidationError(`Routine not found: ${routineName}`, {
26
34
  recoveryHint: "Run a catalog rescan and confirm the routine name.",
@@ -30,52 +38,56 @@ export class RoutineStatusQueryService {
30
38
  }
31
39
  async buildMergedView(limit = 10) {
32
40
  const now = new Date();
33
- let catalog = this.deps.catalogService.getCatalog();
34
- if (catalog.size === 0) {
35
- catalog = (await this.deps.catalogService.rescan()).catalog;
41
+ let snapshot = this.deps.catalogService.getSnapshot();
42
+ if (snapshot.catalog.size === 0 && snapshot.invalid.length === 0) {
43
+ snapshot = await this.deps.catalogService.rescan();
36
44
  }
37
45
  const [states, runtimes, logs] = await Promise.all([
38
46
  this.deps.stateRepo.list(),
39
47
  this.deps.runtimeRepo.list(),
40
48
  this.deps.store.listLogs(),
41
49
  ]);
50
+ const catalog = snapshot.catalog;
42
51
  const stateMap = new Map(states.map((entry) => [entry.name.value, entry]));
43
52
  const runtimeMap = new Map(runtimes.map((entry) => [entry.name, entry.projection]));
53
+ const warningsByName = buildWarningsByName(snapshot);
44
54
  const names = new Set([
45
55
  ...catalog.keys(),
46
56
  ...stateMap.keys(),
47
57
  ...runtimeMap.keys(),
48
58
  ]);
49
- return [...names]
50
- .sort((left, right) => left.localeCompare(right))
51
- .map((name) => {
52
- const catalogEntry = catalog.get(name);
53
- const stateEntry = stateMap.get(name);
54
- const runtime = runtimeMap.get(name);
55
- const recentExecutions = filterRoutineLogs(logs, name).slice(0, limit);
56
- const lastRun = stateEntry?.state.lastExecutedAt?.toISOString() ??
57
- recentExecutions[0]?.timestamp ??
58
- null;
59
- const status = resolveStatus(runtime, stateEntry?.state.lastStatus, this.checkProcessAlive);
60
- return {
61
- name,
62
- status,
63
- nextScheduledAt: stateEntry?.state.nextScheduledAt?.toISOString() ??
64
- catalogEntry?.definition.trigger?.schedule.nextAfter(now)?.toISOString() ??
65
- null,
66
- lastRun,
67
- definition: catalogEntry
68
- ? {
69
- description: catalogEntry.definition.description,
70
- yamlPath: catalogEntry.yamlPath,
71
- scheduled: catalogEntry.definition.isScheduled(),
72
- }
73
- : undefined,
74
- state: stateEntry?.state,
75
- runtime,
76
- recentExecutions,
77
- };
78
- });
59
+ return {
60
+ entries: [...names]
61
+ .sort((left, right) => left.localeCompare(right))
62
+ .map((name) => {
63
+ const catalogEntry = catalog.get(name);
64
+ const stateEntry = stateMap.get(name);
65
+ const runtime = runtimeMap.get(name);
66
+ const recentExecutions = filterRoutineLogs(logs, name).slice(0, limit);
67
+ const lastRun = stateEntry?.state.lastExecutedAt?.toISOString() ?? null;
68
+ const status = resolveStatus(runtime, stateEntry?.state.lastStatus, this.checkProcessAlive, now);
69
+ return {
70
+ name,
71
+ status,
72
+ schedule: catalogEntry?.definition.trigger?.schedule.value ?? null,
73
+ missedPolicy: catalogEntry?.definition.trigger?.missedPolicy ?? null,
74
+ nextScheduledAt: stateEntry?.state.nextScheduledAt?.toISOString() ??
75
+ catalogEntry?.definition.trigger?.schedule.nextAfter(now)?.toISOString() ??
76
+ null,
77
+ lastRun,
78
+ lastScheduledAt: stateEntry?.state.lastScheduledAt?.toISOString() ?? null,
79
+ warnings: warningsByName.get(name) ?? [],
80
+ definition: buildDefinitionDto(catalogEntry),
81
+ state: buildRoutineStateDto(stateEntry),
82
+ runtime,
83
+ recentExecutions,
84
+ };
85
+ }),
86
+ invalid: snapshot.invalid.map((issue) => ({
87
+ yamlPath: issue.yamlPath,
88
+ error: issue.error,
89
+ })),
90
+ };
79
91
  }
80
92
  }
81
93
  function filterRoutineLogs(logs, routineName) {
@@ -86,13 +98,73 @@ function filterRoutineLogs(logs, routineName) {
86
98
  })
87
99
  .sort((left, right) => right.timestamp.localeCompare(left.timestamp));
88
100
  }
89
- function resolveStatus(runtime, lastStatus, checkProcessAlive) {
101
+ function resolveStatus(runtime, lastStatus, checkProcessAlive, now) {
90
102
  if (runtime) {
91
- return checkProcessAlive(runtime.pid) ? "running" : "stale_runtime";
103
+ if (!checkProcessAlive(runtime.pid)) {
104
+ return "stale_runtime";
105
+ }
106
+ if (now.getTime() - new Date(runtime.lastHeartbeatAt).getTime() >
107
+ STALE_HEARTBEAT_THRESHOLD_MS) {
108
+ return "stale_runtime";
109
+ }
110
+ return "running";
92
111
  }
93
- if (lastStatus === "completed" || lastStatus === "failed") {
112
+ if (lastStatus === "completed" || lastStatus === "failed" || lastStatus === "skipped") {
94
113
  return lastStatus;
95
114
  }
96
115
  return "idle";
97
116
  }
117
+ function buildRoutineStateDto(stateEntry) {
118
+ if (!stateEntry) {
119
+ return undefined;
120
+ }
121
+ return {
122
+ routineName: stateEntry.name.value,
123
+ workflowPath: stateEntry.workflowPath,
124
+ lastExecutedAt: stateEntry.state.lastExecutedAt?.toISOString() ?? null,
125
+ lastScheduledAt: stateEntry.state.lastScheduledAt?.toISOString() ?? null,
126
+ lastStatus: stateEntry.state.lastStatus,
127
+ nextScheduledAt: stateEntry.state.nextScheduledAt?.toISOString() ?? null,
128
+ };
129
+ }
130
+ function buildDefinitionDto(catalogEntry) {
131
+ if (!catalogEntry) {
132
+ return null;
133
+ }
134
+ return {
135
+ description: catalogEntry.definition.description ?? null,
136
+ yamlPath: catalogEntry.yamlPath,
137
+ scheduled: catalogEntry.definition.isScheduled(),
138
+ schedule: catalogEntry.definition.trigger?.schedule.value ?? null,
139
+ missedPolicy: catalogEntry.definition.trigger?.missedPolicy ?? null,
140
+ catchUpLimit: catalogEntry.definition.trigger?.catchUpLimit.value ?? null,
141
+ stepSummary: catalogEntry.definition.workflow.steps.map((step) => `${step.id} (${resolveStepKind(step)})`),
142
+ };
143
+ }
144
+ function buildWarningsByName(snapshot) {
145
+ const warnings = new Map();
146
+ for (const issue of snapshot.invalid) {
147
+ if (issue.kind !== "duplicate_name" || !issue.routineName) {
148
+ continue;
149
+ }
150
+ const entry = warnings.get(issue.routineName) ?? [];
151
+ entry.push(formatDuplicateWarning(issue));
152
+ warnings.set(issue.routineName, entry);
153
+ }
154
+ return warnings;
155
+ }
156
+ function formatDuplicateWarning(issue) {
157
+ const ignored = issue.yamlPath;
158
+ const retained = issue.duplicateOf ?? "the earlier file";
159
+ return `Duplicate routine name detected. Ignoring ${ignored} because ${retained} is loaded first.`;
160
+ }
161
+ function resolveStepKind(step) {
162
+ if ("workflow" in step && step.workflow) {
163
+ return "workflow";
164
+ }
165
+ if ("command" in step && step.command) {
166
+ return "command";
167
+ }
168
+ return "prompt";
169
+ }
98
170
  //# sourceMappingURL=routine-status-query-service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"routine-status-query-service.js","sourceRoot":"","sources":["../../src/application/routine-status-query-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routine-status-query-service.js","sourceRoot":"","sources":["../../src/application/routine-status-query-service.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,9 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
2
  import { type ForkOptions } from "node:child_process";
3
3
  import { CoreManagementService } from "../application/core-management-service.js";
4
4
  import { reconcileAbandonedRunningTaskSessions, runStartupHousekeeping } from "../application/housekeeping.js";
5
5
  import { RelayCliService } from "../application/relay-cli-service.js";
6
- import { RoutineDaemonService, type ProcessStatus } from "../application/routine-daemon-service.js";
6
+ import { RoutineDaemonService, type ProcessStatus, type RoutineDigestEvent } from "../application/routine-daemon-service.js";
7
7
  import { RoutineRunService } from "../application/routine-run-service.js";
8
8
  import { RoutineStatusQueryService } from "../application/routine-status-query-service.js";
9
9
  import { TaskService } from "../application/task-service.js";
@@ -29,6 +29,7 @@ type RoutineCommand = Extract<RelayCliCommand, {
29
29
  }>;
30
30
  interface TextWriter {
31
31
  write(chunk: string): unknown;
32
+ isTTY?: boolean;
32
33
  }
33
34
  interface CoreCommandHandlerDependencies {
34
35
  service: Pick<CoreManagementService, "setup" | "update" | "remove">;
@@ -63,6 +64,7 @@ interface RoutineCommandHandlerDependencies {
63
64
  createRoutineDaemonService: (options: {
64
65
  configDir: string;
65
66
  allowCommandRun: boolean;
67
+ onRoutineEvent?: (event: RoutineDigestEvent) => void;
66
68
  }) => Promise<Pick<RoutineDaemonService, "start">>;
67
69
  createRoutineStatusQueryService: (options: {
68
70
  configDir: string;
package/dist/bin/relay.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
2
  import { fork } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { access, readFile, realpath, rm } from "node:fs/promises";
@@ -136,7 +136,7 @@ async function main() {
136
136
  stateRepo: new RoutineStateRepository(store),
137
137
  eventLog: new RoutineEventLogSink(store),
138
138
  });
139
- const createRoutineDaemonService = async ({ configDir, allowCommandRun }) => {
139
+ const createRoutineDaemonService = async ({ configDir, allowCommandRun, onRoutineEvent }) => {
140
140
  const catalogService = new RoutineCatalogService({
141
141
  configDir,
142
142
  workflowRepo,
@@ -163,6 +163,8 @@ async function main() {
163
163
  runService,
164
164
  stateRepo,
165
165
  leaseRepo,
166
+ }, {
167
+ onRoutineEvent,
166
168
  });
167
169
  };
168
170
  const createRoutineStatusQueryService = ({ configDir }) => new RoutineStatusQueryService({
@@ -334,11 +336,19 @@ export async function handleRoutineCommand(command, dependencies) {
334
336
  switch (command.kind) {
335
337
  case "routine-run": {
336
338
  const workflowPath = resolveRoutineWorkflowPath(command.workflowPath, command.configDir, dependencies.cwd);
339
+ const dryRunTime = new Date().toISOString();
340
+ const routineVars = command.dryRun
341
+ ? {
342
+ ...(command.vars ?? {}),
343
+ scheduled_time: dryRunTime,
344
+ trigger_time: dryRunTime,
345
+ }
346
+ : (command.vars ?? {});
337
347
  await dependencies.runStartupHousekeeping(dependencies.store, {
338
348
  protectedLoopStateFile: command.resumeStateFile,
339
349
  });
340
350
  await dependencies.reconcileAbandonedRunningTaskSessions(dependencies.store, dependencies.taskService);
341
- const loadedRoutine = await dependencies.loadRoutine(workflowPath, dependencies.workflowRepo, command.vars ?? {});
351
+ const loadedRoutine = await dependencies.loadRoutine(workflowPath, dependencies.workflowRepo, routineVars);
342
352
  if (command.dryRun) {
343
353
  dependencies.stdout.write(renderRoutineDryRun(loadedRoutine, workflowPath));
344
354
  return 0;
@@ -425,6 +435,14 @@ export async function handleRoutineCommand(command, dependencies) {
425
435
  const service = await dependencies.createRoutineDaemonService({
426
436
  configDir,
427
437
  allowCommandRun: command.allowCommandRun ?? false,
438
+ onRoutineEvent: command.daemon
439
+ ? undefined
440
+ : (event) => {
441
+ if (event.eventType === "routine_state_updated") {
442
+ return;
443
+ }
444
+ dependencies.stdout.write(`${renderRoutineDigestLine(event, dependencies.stdout)}\n`);
445
+ },
428
446
  });
429
447
  await service.start();
430
448
  return 0;
@@ -437,7 +455,7 @@ export async function handleRoutineCommand(command, dependencies) {
437
455
  const service = dependencies.createRoutineStatusQueryService({
438
456
  configDir: resolveRoutineConfigDir(command.configDir, dependencies.store, dependencies.cwd),
439
457
  });
440
- dependencies.stdout.write(renderRoutineList(await service.list()));
458
+ dependencies.stdout.write(renderRoutineList(await service.list(), command.format ?? "table"));
441
459
  return 0;
442
460
  }
443
461
  case "routine-status": {
@@ -445,7 +463,7 @@ export async function handleRoutineCommand(command, dependencies) {
445
463
  configDir: resolveRoutineConfigDir(command.configDir, dependencies.store, dependencies.cwd),
446
464
  });
447
465
  if (!command.name) {
448
- dependencies.stdout.write(renderRoutineList(await service.list()));
466
+ dependencies.stdout.write(renderRoutineList(await service.list(), "table"));
449
467
  return 0;
450
468
  }
451
469
  dependencies.stdout.write(renderRoutineStatus(await service.status(command.name, { limit: command.limit })));
@@ -465,7 +483,7 @@ export function renderHelpText(topic, scriptName = path.basename(fileURLToPath(i
465
483
  ` ${scriptName} routine run <path> [--config <dir>] [--var key=value ...] [--dry-run] [--resume-state <file>] [--force-resume] [--allow-command-run]`,
466
484
  ` ${scriptName} routine start [--config <dir>] [--daemon] [--allow-command-run]`,
467
485
  ` ${scriptName} routine stop`,
468
- ` ${scriptName} routine list [--config <dir>]`,
486
+ ` ${scriptName} routine list [--config <dir>] [--format <table|tsv|json>]`,
469
487
  ` ${scriptName} routine status [name] [--config <dir>] [--limit <n>]`,
470
488
  ` ${scriptName} core <setup|update|remove> [options]`,
471
489
  ` ${scriptName} setup-vscode [--target <code|code-insiders|cursor>] [--uninstall]`,
@@ -549,6 +567,7 @@ function renderRoutineSubcommandHelp(scriptName) {
549
567
  "",
550
568
  "Options:",
551
569
  " --config <dir> Override the routine config directory",
570
+ " --format <fmt> Output format for `routine list` (table, tsv, json)",
552
571
  " --daemon Start the daemon in the background",
553
572
  " --allow-command-run Allow trusted workflows that declare command.run",
554
573
  " --resume-state <file> Resume a loop-capable routine from checkpoint state",
@@ -585,7 +604,7 @@ function enforceAllowCommandRun(workflowPath, capabilitySummary, allowCommandRun
585
604
  }
586
605
  const offendingPaths = capabilitySummary.commandRunWorkflowPaths.join(", ");
587
606
  throw new ValidationError(`Workflow "${path.resolve(workflowPath)}" requires --allow-command-run because it declares command.run in: ${offendingPaths}.`, {
588
- recoveryHint: "Re-run with --allow-command-run only for trusted workflows.",
607
+ recoveryHint: "Re-run with --allow-command-run only for trusted workflows, e.g. `relay routine run workflow.yaml --allow-command-run`.",
589
608
  });
590
609
  }
591
610
  function isRoutineDaemonChild(env) {
@@ -794,8 +813,12 @@ function printLoopResult(result, writer = process.stderr) {
794
813
  function printRoutineRunResult(result, writer = process.stderr) {
795
814
  const lines = [
796
815
  `Routine ${result.status}.`,
797
- `Scheduled time: ${result.scheduledTime}`,
798
- ...(result.triggerTime ? [`Trigger time: ${result.triggerTime}`] : []),
816
+ ...(result.hasTrigger === false
817
+ ? [`Run time: ${result.scheduledTime}`]
818
+ : [
819
+ `Scheduled time: ${result.scheduledTime}`,
820
+ ...(result.triggerTime ? [`Trigger time: ${result.triggerTime}`] : []),
821
+ ]),
799
822
  ...(result.routineRunId ? [`Routine run id: ${result.routineRunId}`] : []),
800
823
  ...(result.workflowExecutionId
801
824
  ? [`Workflow execution id: ${result.workflowExecutionId}`]
@@ -821,21 +844,88 @@ function renderRoutineDryRun(loadedRoutine, workflowPath) {
821
844
  ];
822
845
  return `${lines.join("\n")}\n`;
823
846
  }
824
- function renderRoutineList(entries) {
847
+ function renderRoutineList(result, format = "table") {
848
+ const { entries, invalid } = result;
849
+ if (format === "json") {
850
+ return `${JSON.stringify({ routines: entries, invalid }, null, 2)}\n`;
851
+ }
825
852
  if (entries.length === 0) {
826
- return "No routines found.\n";
853
+ return invalid.length === 0
854
+ ? "No routines found.\n"
855
+ : `No routines found.\n\n${renderInvalidRoutineFiles(invalid)}\n`;
827
856
  }
828
- const lines = entries.map((entry) => `${entry.name}\tstatus=${entry.status}\tnext=${formatOptionalTimestamp(entry.nextScheduledAt)}\tlast=${formatOptionalTimestamp(entry.lastRun)}`);
829
- return `${lines.join("\n")}\n`;
857
+ if (format === "tsv") {
858
+ const lines = [
859
+ "NAME\tSCHEDULE\tMISSED\tLAST_RUN\tNEXT_RUN\tSTATUS",
860
+ ...entries.map((entry) => [
861
+ entry.name,
862
+ formatRoutineSchedule(entry.schedule),
863
+ formatRoutineMissedPolicy(entry.missedPolicy),
864
+ formatOptionalTimestamp(entry.lastRun),
865
+ formatOptionalTimestamp(entry.nextScheduledAt),
866
+ entry.status,
867
+ ].join("\t")),
868
+ ...invalid.map((entry) => `# Skipped: ${path.basename(entry.yamlPath)}: ${entry.error.replace(/\r?\n/g, " | ")}`),
869
+ ];
870
+ return `${lines.join("\n")}\n`;
871
+ }
872
+ const rows = entries.map((entry) => [
873
+ entry.name,
874
+ formatRoutineSchedule(entry.schedule),
875
+ formatRoutineMissedPolicy(entry.missedPolicy),
876
+ formatOptionalTimestamp(entry.lastRun),
877
+ formatOptionalTimestamp(entry.nextScheduledAt),
878
+ entry.status,
879
+ ]);
880
+ const headers = ["NAME", "SCHEDULE", "MISSED", "LAST RUN", "NEXT RUN", "STATUS"];
881
+ const widths = headers.map((header, column) => Math.max(header.length, ...rows.map((row) => row[column].length)));
882
+ const lines = [
883
+ headers.map((header, column) => header.padEnd(widths[column])).join(" "),
884
+ ...rows.map((row) => row.map((cell, column) => cell.padEnd(widths[column])).join(" ")),
885
+ ];
886
+ const sections = [`${lines.join("\n")}\n`];
887
+ const warnings = [...new Set(entries.flatMap((entry) => entry.warnings))];
888
+ if (warnings.length > 0) {
889
+ sections.push(`Warnings:\n${warnings.map((warning) => ` - ${warning}`).join("\n")}\n`);
890
+ }
891
+ if (invalid.length > 0) {
892
+ sections.push(`${renderInvalidRoutineFiles(invalid)}\n`);
893
+ }
894
+ return sections.join("\n");
895
+ }
896
+ function renderInvalidRoutineFiles(invalid) {
897
+ return [
898
+ `Skipped ${invalid.length} file(s):`,
899
+ ...invalid.map((entry) => ` - ${path.basename(entry.yamlPath)}: ${firstLine(entry.error)}`),
900
+ ].join("\n");
901
+ }
902
+ function firstLine(value) {
903
+ return value.split(/\r?\n/u, 1)[0] ?? "";
830
904
  }
831
905
  function renderRoutineStatus(entry) {
906
+ const definition = entry.definition;
907
+ const nextScheduledAt = entry.state?.nextScheduledAt
908
+ ? entry.state.nextScheduledAt
909
+ : entry.nextScheduledAt
910
+ ? `${entry.nextScheduledAt} (estimated)`
911
+ : "-";
832
912
  const lines = [
833
913
  `Name: ${entry.name}`,
834
914
  `Status: ${entry.status}`,
835
- `Next scheduled: ${formatOptionalTimestamp(entry.nextScheduledAt)}`,
836
- `Last run: ${formatOptionalTimestamp(entry.lastRun)}`,
837
- ...(entry.definition?.description ? [`Description: ${entry.definition.description}`] : []),
838
- ...(entry.definition?.yamlPath ? [`YAML: ${entry.definition.yamlPath}`] : []),
915
+ `Description: ${definition?.description ?? "-"}`,
916
+ `YAML: ${definition?.yamlPath ?? "-"}`,
917
+ `Schedule: ${definition?.schedule ?? "manual"}`,
918
+ `Missed policy: ${definition?.missedPolicy ?? "-"}`,
919
+ `Catch-up limit: ${definition?.catchUpLimit ?? "-"}`,
920
+ `Steps: ${definition?.stepSummary.length ? definition.stepSummary.join(", ") : "-"}`,
921
+ `Next scheduled: ${nextScheduledAt}`,
922
+ `Last executed: ${formatOptionalTimestamp(entry.lastRun)}`,
923
+ `Last scheduled: ${formatOptionalTimestamp(entry.lastScheduledAt)}`,
924
+ `Workflow path: ${entry.state?.workflowPath ?? "-"}`,
925
+ `Last status: ${entry.state?.lastStatus ?? "-"}`,
926
+ ...(entry.warnings.length > 0
927
+ ? ["Warnings:", ...entry.warnings.map((warning) => ` - ${warning}`)]
928
+ : []),
839
929
  ...(entry.runtime
840
930
  ? [
841
931
  `Running pid: ${entry.runtime.pid}`,
@@ -845,7 +935,7 @@ function renderRoutineStatus(entry) {
845
935
  "",
846
936
  "Recent events:",
847
937
  ...(entry.recentExecutions.length > 0
848
- ? entry.recentExecutions.map((event) => ` - ${event.timestamp} ${event.event_type}`)
938
+ ? entry.recentExecutions.map((event) => ` - ${renderRoutineEventSummary(event)}`)
849
939
  : [" - none"]),
850
940
  ];
851
941
  return `${lines.join("\n")}\n`;
@@ -853,6 +943,66 @@ function renderRoutineStatus(entry) {
853
943
  function formatOptionalTimestamp(value) {
854
944
  return value ?? "-";
855
945
  }
946
+ function formatRoutineSchedule(value) {
947
+ return value ?? "manual";
948
+ }
949
+ function formatRoutineMissedPolicy(value) {
950
+ return value ?? "-";
951
+ }
952
+ const DIGEST_EVENT_TYPE_WIDTH = 20;
953
+ function renderRoutineDigestLine(event, writer = process.stdout) {
954
+ const timestamp = new Date().toISOString().slice(11, 19);
955
+ const eventType = colorizeDigestEventType(event.eventType.padEnd(DIGEST_EVENT_TYPE_WIDTH), event.eventType, writer);
956
+ return `[${timestamp}] ${eventType} ${event.routineName} ${formatRoutineDigestDetail(event)}`;
957
+ }
958
+ function colorizeDigestEventType(value, eventType, writer) {
959
+ if (!writer.isTTY) {
960
+ return value;
961
+ }
962
+ const color = eventType === "routine_triggered" || eventType === "routine_catch_up"
963
+ ? 36
964
+ : eventType === "routine_completed"
965
+ ? 32
966
+ : eventType === "routine_failed"
967
+ ? 31
968
+ : eventType === "routine_skipped"
969
+ ? 33
970
+ : null;
971
+ return color === null ? value : `\u001B[${color}m${value}\u001B[0m`;
972
+ }
973
+ function formatRoutineDigestDetail(event) {
974
+ if (typeof event.detail.message === "string") {
975
+ return event.detail.message;
976
+ }
977
+ if (typeof event.detail.reason === "string") {
978
+ return `reason=${event.detail.reason}`;
979
+ }
980
+ if (typeof event.detail.scheduledTime === "string") {
981
+ return `scheduled_time=${event.detail.scheduledTime}`;
982
+ }
983
+ if (typeof event.detail.durationMs === "number") {
984
+ return `duration=${formatDigestDuration(event.detail.durationMs)}`;
985
+ }
986
+ if (typeof event.detail.error === "string") {
987
+ return `error="${event.detail.error}"`;
988
+ }
989
+ return Object.entries(event.detail)
990
+ .map(([key, value]) => `${key}=${String(value)}`)
991
+ .join(" ");
992
+ }
993
+ function formatDigestDuration(durationMs) {
994
+ if (durationMs % 1000 === 0) {
995
+ return `${durationMs / 1000}s`;
996
+ }
997
+ return `${(durationMs / 1000).toFixed(1)}s`;
998
+ }
999
+ function renderRoutineEventSummary(event) {
1000
+ const detail = (event.details?.routine ?? {});
1001
+ if (event.event_type === "routine_failed") {
1002
+ return `${event.timestamp} ${event.event_type} error_code=${detail.error_code ?? "-"} error_message=${detail.error_message ?? "-"} workflow_execution_id=${detail.workflow_execution_id ?? "-"}`;
1003
+ }
1004
+ return `${event.timestamp} ${event.event_type}`;
1005
+ }
856
1006
  function renderDryRun(workflow, workflowPath) {
857
1007
  const lines = [];
858
1008
  lines.push(`Workflow: ${workflow.name}`);
@@ -1034,6 +1184,9 @@ function handleFatalError(error) {
1034
1184
  const message = error instanceof Error ? error.message : "agentic-relay failed unexpectedly.";
1035
1185
  if (error instanceof ValidationError) {
1036
1186
  console.error(message);
1187
+ if (error.recoveryHint) {
1188
+ console.error(`Hint: ${error.recoveryHint}`);
1189
+ }
1037
1190
  process.exitCode = 1;
1038
1191
  return;
1039
1192
  }