@poncho-ai/harness 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.12.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.13.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
3
3
  > tsup src/index.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 158.98 KB
11
- ESM ⚡️ Build success in 59ms
10
+ ESM dist/index.js 161.82 KB
11
+ ESM ⚡️ Build success in 68ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 4596ms
14
- DTS dist/index.d.ts 19.34 KB
13
+ DTS ⚡️ Build success in 4388ms
14
+ DTS dist/index.d.ts 19.54 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#8](https://github.com/cesr/poncho-ai/pull/8) [`658bc54`](https://github.com/cesr/poncho-ai/commit/658bc54d391cb0b58aa678a2b86cd617eebdd8aa) Thanks [@cesr](https://github.com/cesr)! - Add cron job support for scheduled agent tasks. Define recurring jobs in AGENT.md frontmatter with schedule, task, and optional timezone. Includes in-process scheduler for local dev with hot-reload, HTTP endpoint for Vercel/serverless with self-continuation, Vercel scaffold generation with drift detection, and full tool activity tracking in cron conversations.
8
+
3
9
  ## 0.12.0
4
10
 
5
11
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -13,6 +13,11 @@ interface AgentLimitsConfig {
13
13
  maxSteps?: number;
14
14
  timeout?: number;
15
15
  }
16
+ interface CronJobConfig {
17
+ schedule: string;
18
+ task: string;
19
+ timezone?: string;
20
+ }
16
21
  interface AgentFrontmatter {
17
22
  name: string;
18
23
  id?: string;
@@ -27,6 +32,7 @@ interface AgentFrontmatter {
27
32
  mcp?: string[];
28
33
  scripts?: string[];
29
34
  };
35
+ cron?: Record<string, CronJobConfig>;
30
36
  }
31
37
  interface ParsedAgent {
32
38
  frontmatter: AgentFrontmatter;
@@ -374,6 +380,7 @@ declare class AgentHarness {
374
380
  private registerConfiguredBuiltInTools;
375
381
  private shouldEnableWriteTool;
376
382
  constructor(options?: HarnessOptions);
383
+ get frontmatter(): AgentFrontmatter | undefined;
377
384
  private listActiveSkills;
378
385
  private getAgentMcpIntent;
379
386
  private getAgentScriptIntent;
@@ -547,4 +554,4 @@ declare class ToolDispatcher {
547
554
  executeBatch(calls: ToolCall[], context: ToolContext): Promise<ToolExecutionResult[]>;
548
555
  }
549
556
 
550
- export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type TelemetryConfig, TelemetryEmitter, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
557
+ export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type TelemetryConfig, TelemetryEmitter, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
package/dist/index.js CHANGED
@@ -67,6 +67,59 @@ var matchesSlashPattern = (value, pattern) => {
67
67
  var FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
68
68
  var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
69
69
  var asNumberOrUndefined = (value) => typeof value === "number" ? value : void 0;
70
+ var CRON_EXPRESSION_PATTERN = /^(\S+\s+){4}\S+$/;
71
+ var validateCronExpression = (expr, path) => {
72
+ if (!CRON_EXPRESSION_PATTERN.test(expr.trim())) {
73
+ throw new Error(
74
+ `Invalid cron expression at ${path}: "${expr}". Expected 5-field cron format (minute hour day month weekday).`
75
+ );
76
+ }
77
+ };
78
+ var KNOWN_TIMEZONES = (() => {
79
+ try {
80
+ return new Set(Intl.supportedValuesOf("timeZone"));
81
+ } catch {
82
+ return null;
83
+ }
84
+ })();
85
+ var validateTimezone = (tz, path) => {
86
+ if (KNOWN_TIMEZONES && !KNOWN_TIMEZONES.has(tz)) {
87
+ throw new Error(
88
+ `Invalid timezone at ${path}: "${tz}". Expected an IANA timezone string (e.g. "America/New_York", "UTC").`
89
+ );
90
+ }
91
+ };
92
+ var parseCronJobs = (value) => {
93
+ const raw = asRecord(value);
94
+ const keys = Object.keys(raw);
95
+ if (keys.length === 0) return void 0;
96
+ const jobs = {};
97
+ for (const jobName of keys) {
98
+ const jobValue = asRecord(raw[jobName]);
99
+ const path = `AGENT.md frontmatter cron.${jobName}`;
100
+ if (typeof jobValue.schedule !== "string" || jobValue.schedule.trim() === "") {
101
+ throw new Error(
102
+ `Invalid ${path}: "schedule" is required and must be a non-empty string.`
103
+ );
104
+ }
105
+ if (typeof jobValue.task !== "string" || jobValue.task.trim() === "") {
106
+ throw new Error(
107
+ `Invalid ${path}: "task" is required and must be a non-empty string.`
108
+ );
109
+ }
110
+ validateCronExpression(jobValue.schedule, path);
111
+ const timezone = typeof jobValue.timezone === "string" && jobValue.timezone.trim() ? jobValue.timezone.trim() : void 0;
112
+ if (timezone) {
113
+ validateTimezone(timezone, path);
114
+ }
115
+ jobs[jobName] = {
116
+ schedule: jobValue.schedule.trim(),
117
+ task: jobValue.task,
118
+ timezone
119
+ };
120
+ }
121
+ return jobs;
122
+ };
70
123
  var parseAgentMarkdown = (content) => {
71
124
  const match = content.match(FRONTMATTER_PATTERN);
72
125
  if (!match) {
@@ -144,7 +197,8 @@ var parseAgentMarkdown = (content) => {
144
197
  approvalRequired: approvalRequired.mcp.length > 0 || approvalRequired.scripts.length > 0 ? {
145
198
  mcp: approvalRequired.mcp.length > 0 ? approvalRequired.mcp : void 0,
146
199
  scripts: approvalRequired.scripts.length > 0 ? approvalRequired.scripts : void 0
147
- } : void 0
200
+ } : void 0,
201
+ cron: parseCronJobs(parsed.cron)
148
202
  };
149
203
  return {
150
204
  frontmatter,
@@ -2559,6 +2613,25 @@ You can extend your own capabilities by creating custom JavaScript/TypeScript sc
2559
2613
  - Script entries outside \`./scripts/\` must also appear in \`allowed-tools\`.
2560
2614
  - Keep MCP server connection details (\`url\`, auth env vars) in \`poncho.config.js\` only.
2561
2615
 
2616
+ ## Cron Jobs
2617
+
2618
+ Users can define scheduled tasks in \`AGENT.md\` frontmatter:
2619
+
2620
+ \`\`\`yaml
2621
+ cron:
2622
+ daily-report:
2623
+ schedule: "0 9 * * *" # Standard 5-field cron expression
2624
+ timezone: "America/New_York" # Optional IANA timezone (default: UTC)
2625
+ task: "Generate the daily sales report"
2626
+ \`\`\`
2627
+
2628
+ - Each cron job triggers an autonomous agent run with the specified task, creating a fresh conversation.
2629
+ - In \`poncho dev\`, jobs run via an in-process scheduler and appear in the web UI sidebar (prefixed with \`[cron]\`).
2630
+ - For Vercel: \`poncho build vercel\` generates \`vercel.json\` cron entries. Set \`CRON_SECRET\` = \`PONCHO_AUTH_TOKEN\`.
2631
+ - Jobs can also be triggered manually: \`GET /api/cron/<jobName>\`.
2632
+ - To carry context across cron runs, enable memory.
2633
+ - **IMPORTANT**: When adding a new cron job, always PRESERVE all existing cron jobs. Never remove or overwrite existing jobs unless the user explicitly asks you to replace or delete them. Read the full current \`cron:\` block before editing, and append the new job alongside the existing ones.
2634
+
2562
2635
  ## When users ask about customization:
2563
2636
 
2564
2637
  - Explain and edit \`poncho.config.js\` for model/provider, storage+memory, auth, telemetry, and MCP settings.
@@ -2654,6 +2727,9 @@ var AgentHarness = class {
2654
2727
  this.dispatcher.registerMany(options.toolDefinitions);
2655
2728
  }
2656
2729
  }
2730
+ get frontmatter() {
2731
+ return this.parsedAgent?.frontmatter;
2732
+ }
2657
2733
  listActiveSkills() {
2658
2734
  return [...this.activeSkillNames].sort();
2659
2735
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,6 +22,12 @@ export interface AgentLimitsConfig {
22
22
  timeout?: number;
23
23
  }
24
24
 
25
+ export interface CronJobConfig {
26
+ schedule: string;
27
+ task: string;
28
+ timezone?: string;
29
+ }
30
+
25
31
  export interface AgentFrontmatter {
26
32
  name: string;
27
33
  id?: string;
@@ -36,6 +42,7 @@ export interface AgentFrontmatter {
36
42
  mcp?: string[];
37
43
  scripts?: string[];
38
44
  };
45
+ cron?: Record<string, CronJobConfig>;
39
46
  }
40
47
 
41
48
  export interface ParsedAgent {
@@ -63,6 +70,74 @@ const asRecord = (value: unknown): Record<string, unknown> =>
63
70
  const asNumberOrUndefined = (value: unknown): number | undefined =>
64
71
  typeof value === "number" ? value : undefined;
65
72
 
73
+ const CRON_EXPRESSION_PATTERN = /^(\S+\s+){4}\S+$/;
74
+
75
+ const validateCronExpression = (expr: string, path: string): void => {
76
+ if (!CRON_EXPRESSION_PATTERN.test(expr.trim())) {
77
+ throw new Error(
78
+ `Invalid cron expression at ${path}: "${expr}". Expected 5-field cron format (minute hour day month weekday).`,
79
+ );
80
+ }
81
+ };
82
+
83
+ const KNOWN_TIMEZONES: Set<string> | null = (() => {
84
+ try {
85
+ return new Set(Intl.supportedValuesOf("timeZone"));
86
+ } catch {
87
+ return null;
88
+ }
89
+ })();
90
+
91
+ const validateTimezone = (tz: string, path: string): void => {
92
+ if (KNOWN_TIMEZONES && !KNOWN_TIMEZONES.has(tz)) {
93
+ throw new Error(
94
+ `Invalid timezone at ${path}: "${tz}". Expected an IANA timezone string (e.g. "America/New_York", "UTC").`,
95
+ );
96
+ }
97
+ };
98
+
99
+ const parseCronJobs = (
100
+ value: unknown,
101
+ ): Record<string, CronJobConfig> | undefined => {
102
+ const raw = asRecord(value);
103
+ const keys = Object.keys(raw);
104
+ if (keys.length === 0) return undefined;
105
+
106
+ const jobs: Record<string, CronJobConfig> = {};
107
+ for (const jobName of keys) {
108
+ const jobValue = asRecord(raw[jobName]);
109
+ const path = `AGENT.md frontmatter cron.${jobName}`;
110
+
111
+ if (typeof jobValue.schedule !== "string" || jobValue.schedule.trim() === "") {
112
+ throw new Error(
113
+ `Invalid ${path}: "schedule" is required and must be a non-empty string.`,
114
+ );
115
+ }
116
+ if (typeof jobValue.task !== "string" || jobValue.task.trim() === "") {
117
+ throw new Error(
118
+ `Invalid ${path}: "task" is required and must be a non-empty string.`,
119
+ );
120
+ }
121
+
122
+ validateCronExpression(jobValue.schedule, path);
123
+
124
+ const timezone =
125
+ typeof jobValue.timezone === "string" && jobValue.timezone.trim()
126
+ ? jobValue.timezone.trim()
127
+ : undefined;
128
+ if (timezone) {
129
+ validateTimezone(timezone, path);
130
+ }
131
+
132
+ jobs[jobName] = {
133
+ schedule: jobValue.schedule.trim(),
134
+ task: jobValue.task,
135
+ timezone,
136
+ };
137
+ }
138
+ return jobs;
139
+ };
140
+
66
141
  export const parseAgentMarkdown = (content: string): ParsedAgent => {
67
142
  const match = content.match(FRONTMATTER_PATTERN);
68
143
 
@@ -175,6 +250,7 @@ export const parseAgentMarkdown = (content: string): ParsedAgent => {
175
250
  : undefined,
176
251
  }
177
252
  : undefined,
253
+ cron: parseCronJobs(parsed.cron),
178
254
  };
179
255
 
180
256
  return {
package/src/harness.ts CHANGED
@@ -13,7 +13,7 @@ import type {
13
13
  import { getTextContent } from "@poncho-ai/sdk";
14
14
  import type { UploadStore } from "./upload-store.js";
15
15
  import { PONCHO_UPLOAD_SCHEME, deriveUploadKey } from "./upload-store.js";
16
- import { parseAgentFile, renderAgentPrompt, type ParsedAgent } from "./agent-parser.js";
16
+ import { parseAgentFile, renderAgentPrompt, type ParsedAgent, type AgentFrontmatter } from "./agent-parser.js";
17
17
  import { loadPonchoConfig, resolveMemoryConfig, type PonchoConfig } from "./config.js";
18
18
  import { createDefaultTools, createWriteTool } from "./default-tools.js";
19
19
  import {
@@ -236,6 +236,25 @@ You can extend your own capabilities by creating custom JavaScript/TypeScript sc
236
236
  - Script entries outside \`./scripts/\` must also appear in \`allowed-tools\`.
237
237
  - Keep MCP server connection details (\`url\`, auth env vars) in \`poncho.config.js\` only.
238
238
 
239
+ ## Cron Jobs
240
+
241
+ Users can define scheduled tasks in \`AGENT.md\` frontmatter:
242
+
243
+ \`\`\`yaml
244
+ cron:
245
+ daily-report:
246
+ schedule: "0 9 * * *" # Standard 5-field cron expression
247
+ timezone: "America/New_York" # Optional IANA timezone (default: UTC)
248
+ task: "Generate the daily sales report"
249
+ \`\`\`
250
+
251
+ - Each cron job triggers an autonomous agent run with the specified task, creating a fresh conversation.
252
+ - In \`poncho dev\`, jobs run via an in-process scheduler and appear in the web UI sidebar (prefixed with \`[cron]\`).
253
+ - For Vercel: \`poncho build vercel\` generates \`vercel.json\` cron entries. Set \`CRON_SECRET\` = \`PONCHO_AUTH_TOKEN\`.
254
+ - Jobs can also be triggered manually: \`GET /api/cron/<jobName>\`.
255
+ - To carry context across cron runs, enable memory.
256
+ - **IMPORTANT**: When adding a new cron job, always PRESERVE all existing cron jobs. Never remove or overwrite existing jobs unless the user explicitly asks you to replace or delete them. Read the full current \`cron:\` block before editing, and append the new job alongside the existing ones.
257
+
239
258
  ## When users ask about customization:
240
259
 
241
260
  - Explain and edit \`poncho.config.js\` for model/provider, storage+memory, auth, telemetry, and MCP settings.
@@ -344,6 +363,10 @@ export class AgentHarness {
344
363
  }
345
364
  }
346
365
 
366
+ get frontmatter(): AgentFrontmatter | undefined {
367
+ return this.parsedAgent?.frontmatter;
368
+ }
369
+
347
370
  private listActiveSkills(): string[] {
348
371
  return [...this.activeSkillNames].sort();
349
372
  }
@@ -37,6 +37,124 @@ Env: {{runtime.environment}}
37
37
  expect(prompt).toContain("Env: development");
38
38
  });
39
39
 
40
+ describe("cron jobs", () => {
41
+ it("parses cron jobs from frontmatter", () => {
42
+ const parsed = parseAgentMarkdown(`---
43
+ name: test-agent
44
+ cron:
45
+ daily-report:
46
+ schedule: "0 9 * * *"
47
+ task: "Generate the daily report"
48
+ health-check:
49
+ schedule: "*/30 * * * *"
50
+ timezone: "America/New_York"
51
+ task: "Check all APIs"
52
+ ---
53
+
54
+ # Agent
55
+ `);
56
+ expect(parsed.frontmatter.cron).toBeDefined();
57
+ expect(Object.keys(parsed.frontmatter.cron!)).toEqual([
58
+ "daily-report",
59
+ "health-check",
60
+ ]);
61
+ expect(parsed.frontmatter.cron!["daily-report"]).toEqual({
62
+ schedule: "0 9 * * *",
63
+ task: "Generate the daily report",
64
+ timezone: undefined,
65
+ });
66
+ expect(parsed.frontmatter.cron!["health-check"]).toEqual({
67
+ schedule: "*/30 * * * *",
68
+ task: "Check all APIs",
69
+ timezone: "America/New_York",
70
+ });
71
+ });
72
+
73
+ it("returns undefined cron when not defined", () => {
74
+ const parsed = parseAgentMarkdown(`---
75
+ name: test-agent
76
+ ---
77
+
78
+ # Agent
79
+ `);
80
+ expect(parsed.frontmatter.cron).toBeUndefined();
81
+ });
82
+
83
+ it("throws on missing schedule", () => {
84
+ expect(() =>
85
+ parseAgentMarkdown(`---
86
+ name: test-agent
87
+ cron:
88
+ bad-job:
89
+ task: "Do something"
90
+ ---
91
+
92
+ # Agent
93
+ `),
94
+ ).toThrow(/"schedule" is required/);
95
+ });
96
+
97
+ it("throws on missing task", () => {
98
+ expect(() =>
99
+ parseAgentMarkdown(`---
100
+ name: test-agent
101
+ cron:
102
+ bad-job:
103
+ schedule: "0 9 * * *"
104
+ ---
105
+
106
+ # Agent
107
+ `),
108
+ ).toThrow(/"task" is required/);
109
+ });
110
+
111
+ it("throws on invalid cron expression", () => {
112
+ expect(() =>
113
+ parseAgentMarkdown(`---
114
+ name: test-agent
115
+ cron:
116
+ bad-job:
117
+ schedule: "every day"
118
+ task: "Do something"
119
+ ---
120
+
121
+ # Agent
122
+ `),
123
+ ).toThrow(/Invalid cron expression/);
124
+ });
125
+
126
+ it("throws on invalid timezone", () => {
127
+ expect(() =>
128
+ parseAgentMarkdown(`---
129
+ name: test-agent
130
+ cron:
131
+ bad-job:
132
+ schedule: "0 9 * * *"
133
+ timezone: "Fake/Zone"
134
+ task: "Do something"
135
+ ---
136
+
137
+ # Agent
138
+ `),
139
+ ).toThrow(/Invalid timezone/);
140
+ });
141
+
142
+ it("accepts valid timezone", () => {
143
+ const parsed = parseAgentMarkdown(`---
144
+ name: test-agent
145
+ cron:
146
+ job:
147
+ schedule: "0 9 * * *"
148
+ timezone: "Europe/London"
149
+ task: "Do something"
150
+ ---
151
+
152
+ # Agent
153
+ `);
154
+ expect(parsed.frontmatter.cron!["job"]!.timezone).toBe("Europe/London");
155
+ });
156
+ });
157
+
40
158
  it("parses approval-required with relative script paths", () => {
41
159
  const parsed = parseAgentMarkdown(`---
42
160
  name: test-agent