clementine-agent 1.18.122 → 1.18.123
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.
- package/dist/gateway/cron-scheduler.js +95 -132
- package/package.json +1 -1
|
@@ -79,6 +79,87 @@ function normalizeStringArray(v) {
|
|
|
79
79
|
const out = Array.from(new Set(v.map(x => typeof x === 'string' ? x.trim() : '').filter(Boolean)));
|
|
80
80
|
return out.length > 0 ? out : undefined;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Single-source-of-truth YAML → CronJobDefinition parser.
|
|
84
|
+
*
|
|
85
|
+
* Used by parseCronJobs (global CRON.md) AND parseAgentCronJobs (per-agent).
|
|
86
|
+
* Caller is responsible for handling the name prefix (agent jobs are
|
|
87
|
+
* prefixed with `<slug>:`) and for tagging agentSlug.
|
|
88
|
+
*
|
|
89
|
+
* Returns null when the row is malformed (missing name/schedule/prompt) —
|
|
90
|
+
* the caller logs + skips. Accepts both snake_case and camelCase YAML keys
|
|
91
|
+
* since users hand-edit CRON.md and we want to be forgiving.
|
|
92
|
+
*
|
|
93
|
+
* Centralizing here closes the drift the previous audit flagged: the agent
|
|
94
|
+
* variant was missing alwaysDeliver/attachments/requiresConfirmation/
|
|
95
|
+
* confirmationTimeoutMin and the description field. After this refactor
|
|
96
|
+
* both surfaces have the same field set.
|
|
97
|
+
*/
|
|
98
|
+
function parseJobYaml(job) {
|
|
99
|
+
const name = String(job.name ?? '');
|
|
100
|
+
const schedule = String(job.schedule ?? '');
|
|
101
|
+
const prompt = String(job.prompt ?? '');
|
|
102
|
+
if (!name || !schedule || !prompt)
|
|
103
|
+
return null;
|
|
104
|
+
const enabled = job.enabled !== false;
|
|
105
|
+
const tier = Number(job.tier ?? 1);
|
|
106
|
+
const maxTurns = job.max_turns != null ? Number(job.max_turns) : undefined;
|
|
107
|
+
const model = job.model != null ? String(job.model) : undefined;
|
|
108
|
+
const workDir = job.work_dir != null ? String(job.work_dir) : undefined;
|
|
109
|
+
const mode = job.mode === 'unleashed' ? 'unleashed' : 'standard';
|
|
110
|
+
const maxHours = job.max_hours != null ? Number(job.max_hours) : undefined;
|
|
111
|
+
const maxRetries = job.max_retries != null ? Number(job.max_retries) : undefined;
|
|
112
|
+
const after = job.after != null ? String(job.after) : undefined;
|
|
113
|
+
const successCriteria = Array.isArray(job.success_criteria)
|
|
114
|
+
? job.success_criteria.map(c => String(c))
|
|
115
|
+
: undefined;
|
|
116
|
+
// Prefer free-form successCriteriaText; fall back to joining the legacy
|
|
117
|
+
// string[] so legacy YAML keeps rendering. Writes go to the new field.
|
|
118
|
+
let successCriteriaText = typeof job.success_criteria_text === 'string'
|
|
119
|
+
? String(job.success_criteria_text)
|
|
120
|
+
: (typeof job.successCriteriaText === 'string' ? String(job.successCriteriaText) : undefined);
|
|
121
|
+
if (!successCriteriaText && Array.isArray(successCriteria) && successCriteria.length > 0) {
|
|
122
|
+
successCriteriaText = successCriteria.join('\n');
|
|
123
|
+
}
|
|
124
|
+
const successSchemaRaw = job.success_schema ?? job.successSchema;
|
|
125
|
+
const successSchema = (successSchemaRaw && typeof successSchemaRaw === 'object' && !Array.isArray(successSchemaRaw))
|
|
126
|
+
? successSchemaRaw
|
|
127
|
+
: undefined;
|
|
128
|
+
const addDirs = normalizeStringArray(job.add_dirs ?? job.addDirs);
|
|
129
|
+
const alwaysDeliver = job.always_deliver === true ? true : undefined;
|
|
130
|
+
const context = job.context != null ? String(job.context) : undefined;
|
|
131
|
+
const preCheck = job.pre_check != null ? String(job.pre_check) : undefined;
|
|
132
|
+
const attachments = normalizeStringArray(job.attachments);
|
|
133
|
+
const requiresConfirmation = job.requires_confirmation === true || job.requiresConfirmation === true ? true : undefined;
|
|
134
|
+
const confirmationTimeoutMin = job.confirmation_timeout_min != null
|
|
135
|
+
? Number(job.confirmation_timeout_min)
|
|
136
|
+
: (job.confirmationTimeoutMin != null ? Number(job.confirmationTimeoutMin) : undefined);
|
|
137
|
+
// Per-job agent scoping (a global cron can be scoped to a specific
|
|
138
|
+
// agent's profile). Accept both casings.
|
|
139
|
+
const agentSlugRaw = job.agentSlug ?? job.agent_slug;
|
|
140
|
+
const agentSlug = typeof agentSlugRaw === 'string' && /^[a-z0-9-]+$/i.test(agentSlugRaw)
|
|
141
|
+
? agentSlugRaw
|
|
142
|
+
: undefined;
|
|
143
|
+
const skills = normalizeStringArray(job.skills);
|
|
144
|
+
const allowedTools = normalizeStringArray(job.allowed_tools ?? job.allowedTools);
|
|
145
|
+
const allowedMcpServers = normalizeStringArray(job.allowed_mcp_servers ?? job.allowedMcpServers);
|
|
146
|
+
const tags = normalizeStringArray(job.tags);
|
|
147
|
+
const categoryRaw = job.category;
|
|
148
|
+
const category = typeof categoryRaw === 'string' && categoryRaw.trim()
|
|
149
|
+
? categoryRaw.trim().slice(0, 64)
|
|
150
|
+
: undefined;
|
|
151
|
+
const predictable = typeof job.predictable === 'boolean' ? job.predictable : undefined;
|
|
152
|
+
const description = typeof job.description === 'string' && job.description.trim()
|
|
153
|
+
? job.description.trim().slice(0, 500)
|
|
154
|
+
: undefined;
|
|
155
|
+
return {
|
|
156
|
+
name, schedule, prompt, enabled, tier, description, maxTurns, model, workDir, mode,
|
|
157
|
+
maxHours, maxRetries, after, successCriteria, successCriteriaText, successSchema, addDirs,
|
|
158
|
+
alwaysDeliver, context, preCheck, attachments, requiresConfirmation, confirmationTimeoutMin,
|
|
159
|
+
agentSlug,
|
|
160
|
+
skills, allowedTools, allowedMcpServers, tags, category, predictable,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
82
163
|
/**
|
|
83
164
|
* Parse cron job definitions from vault/00-System/CRON.md frontmatter.
|
|
84
165
|
* Used by both the in-process CronScheduler and the standalone CLI runner.
|
|
@@ -86,10 +167,9 @@ function normalizeStringArray(v) {
|
|
|
86
167
|
export function parseCronJobs() {
|
|
87
168
|
if (!existsSync(CRON_FILE))
|
|
88
169
|
return [];
|
|
89
|
-
const raw = readFileSync(CRON_FILE, 'utf-8');
|
|
90
170
|
let parsed;
|
|
91
171
|
try {
|
|
92
|
-
parsed = matter(
|
|
172
|
+
parsed = matter(readFileSync(CRON_FILE, 'utf-8'));
|
|
93
173
|
}
|
|
94
174
|
catch (err) {
|
|
95
175
|
logger.error({ err }, 'CRON.md YAML parse error — keeping previous jobs. Fix the file manually.');
|
|
@@ -98,76 +178,11 @@ export function parseCronJobs() {
|
|
|
98
178
|
const jobDefs = (parsed.data.jobs ?? []);
|
|
99
179
|
const jobs = [];
|
|
100
180
|
for (const job of jobDefs) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const tier = Number(job.tier ?? 1);
|
|
106
|
-
const maxTurns = job.max_turns != null ? Number(job.max_turns) : undefined;
|
|
107
|
-
const model = job.model != null ? String(job.model) : undefined;
|
|
108
|
-
const workDir = job.work_dir != null ? String(job.work_dir) : undefined;
|
|
109
|
-
const mode = job.mode === 'unleashed' ? 'unleashed' : 'standard';
|
|
110
|
-
const maxHours = job.max_hours != null ? Number(job.max_hours) : undefined;
|
|
111
|
-
const maxRetries = job.max_retries != null ? Number(job.max_retries) : undefined;
|
|
112
|
-
const after = job.after != null ? String(job.after) : undefined;
|
|
113
|
-
const successCriteria = Array.isArray(job.success_criteria)
|
|
114
|
-
? job.success_criteria.map(c => String(c))
|
|
115
|
-
: undefined;
|
|
116
|
-
// PRD Phase 1: prefer success_criteria_text (free-form). On read, fall
|
|
117
|
-
// back to joining the legacy success_criteria string[] so legacy YAML
|
|
118
|
-
// keeps rendering in the new editor surface. Writes go to the new field.
|
|
119
|
-
let successCriteriaText = typeof job.success_criteria_text === 'string'
|
|
120
|
-
? String(job.success_criteria_text)
|
|
121
|
-
: (typeof job.successCriteriaText === 'string' ? String(job.successCriteriaText) : undefined);
|
|
122
|
-
if (!successCriteriaText && Array.isArray(successCriteria) && successCriteria.length > 0) {
|
|
123
|
-
successCriteriaText = successCriteria.join('\n');
|
|
124
|
-
}
|
|
125
|
-
// PRD Phase 1: JSON Schema validated against ResultMessage.structured_output.
|
|
126
|
-
// Accept either snake_case (success_schema) or camelCase from API. Stored
|
|
127
|
-
// as a plain object; ajv is loaded lazily at validation time.
|
|
128
|
-
const successSchemaRaw = job.success_schema ?? job.successSchema;
|
|
129
|
-
const successSchema = (successSchemaRaw && typeof successSchemaRaw === 'object' && !Array.isArray(successSchemaRaw))
|
|
130
|
-
? successSchemaRaw
|
|
131
|
-
: undefined;
|
|
132
|
-
// PRD Phase 1: read scope beyond cwd. Accept either casing.
|
|
133
|
-
const addDirs = normalizeStringArray(job.add_dirs ?? job.addDirs);
|
|
134
|
-
const alwaysDeliver = job.always_deliver === true ? true : undefined;
|
|
135
|
-
const context = job.context != null ? String(job.context) : undefined;
|
|
136
|
-
const preCheck = job.pre_check != null ? String(job.pre_check) : undefined;
|
|
137
|
-
// Optional: scope a global job to a specific agent's profile (loads
|
|
138
|
-
// the agent's allowedTools whitelist, system prompt, etc.). Accept
|
|
139
|
-
// both camelCase and snake_case to be forgiving of user-written YAML.
|
|
140
|
-
const agentSlugRaw = job.agentSlug ?? job.agent_slug;
|
|
141
|
-
const agentSlug = typeof agentSlugRaw === 'string' && /^[a-z0-9-]+$/i.test(agentSlugRaw)
|
|
142
|
-
? agentSlugRaw
|
|
143
|
-
: undefined;
|
|
144
|
-
// ── Trick capabilities — accept both camelCase and snake_case ─────
|
|
145
|
-
const skills = normalizeStringArray(job.skills);
|
|
146
|
-
const allowedTools = normalizeStringArray(job.allowed_tools ?? job.allowedTools);
|
|
147
|
-
const allowedMcpServers = normalizeStringArray(job.allowed_mcp_servers ?? job.allowedMcpServers);
|
|
148
|
-
const tags = normalizeStringArray(job.tags);
|
|
149
|
-
const categoryRaw = job.category;
|
|
150
|
-
const category = typeof categoryRaw === 'string' && categoryRaw.trim()
|
|
151
|
-
? categoryRaw.trim().slice(0, 64)
|
|
152
|
-
: undefined;
|
|
153
|
-
// Predictable (contract) mode — undefined means legacy behavior.
|
|
154
|
-
const predictable = typeof job.predictable === 'boolean' ? job.predictable : undefined;
|
|
155
|
-
// 1.18.119 — human-readable description (used by the task card preview
|
|
156
|
-
// and by the cron-clean migrator to surface what each job does without
|
|
157
|
-
// showing raw prompt boilerplate).
|
|
158
|
-
const description = typeof job.description === 'string' && job.description.trim()
|
|
159
|
-
? job.description.trim().slice(0, 500)
|
|
160
|
-
: undefined;
|
|
161
|
-
if (!name || !schedule || !prompt) {
|
|
181
|
+
const def = parseJobYaml(job);
|
|
182
|
+
if (def)
|
|
183
|
+
jobs.push(def);
|
|
184
|
+
else
|
|
162
185
|
logger.warn({ job }, 'Skipping malformed cron job');
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
jobs.push({
|
|
166
|
-
name, schedule, prompt, enabled, tier, description, maxTurns, model, workDir, mode,
|
|
167
|
-
maxHours, maxRetries, after, successCriteria, successCriteriaText, successSchema, addDirs,
|
|
168
|
-
alwaysDeliver, context, preCheck, agentSlug,
|
|
169
|
-
skills, allowedTools, allowedMcpServers, tags, category, predictable,
|
|
170
|
-
});
|
|
171
186
|
}
|
|
172
187
|
return jobs;
|
|
173
188
|
}
|
|
@@ -193,72 +208,20 @@ export function parseAgentCronJobs(agentsDir) {
|
|
|
193
208
|
if (!existsSync(cronFile))
|
|
194
209
|
continue;
|
|
195
210
|
try {
|
|
196
|
-
const
|
|
197
|
-
const parsed = matter(raw);
|
|
211
|
+
const parsed = matter(readFileSync(cronFile, 'utf-8'));
|
|
198
212
|
const jobDefs = (parsed.data.jobs ?? []);
|
|
199
213
|
for (const job of jobDefs) {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
const prompt = String(job.prompt ?? '');
|
|
203
|
-
const enabled = job.enabled !== false;
|
|
204
|
-
const tier = Number(job.tier ?? 1);
|
|
205
|
-
const maxTurns = job.max_turns != null ? Number(job.max_turns) : undefined;
|
|
206
|
-
const model = job.model != null ? String(job.model) : undefined;
|
|
207
|
-
const workDir = job.work_dir != null ? String(job.work_dir) : undefined;
|
|
208
|
-
const mode = job.mode === 'unleashed' ? 'unleashed' : 'standard';
|
|
209
|
-
const maxHours = job.max_hours != null ? Number(job.max_hours) : undefined;
|
|
210
|
-
const maxRetries = job.max_retries != null ? Number(job.max_retries) : undefined;
|
|
211
|
-
const after = job.after != null ? String(job.after) : undefined;
|
|
212
|
-
const successCriteria = Array.isArray(job.success_criteria)
|
|
213
|
-
? job.success_criteria.map(c => String(c))
|
|
214
|
-
: undefined;
|
|
215
|
-
// PRD Phase 1 fields — symmetric with global parser above.
|
|
216
|
-
let successCriteriaText = typeof job.success_criteria_text === 'string'
|
|
217
|
-
? String(job.success_criteria_text)
|
|
218
|
-
: (typeof job.successCriteriaText === 'string' ? String(job.successCriteriaText) : undefined);
|
|
219
|
-
if (!successCriteriaText && Array.isArray(successCriteria) && successCriteria.length > 0) {
|
|
220
|
-
successCriteriaText = successCriteria.join('\n');
|
|
221
|
-
}
|
|
222
|
-
const successSchemaRaw = job.success_schema ?? job.successSchema;
|
|
223
|
-
const successSchema = (successSchemaRaw && typeof successSchemaRaw === 'object' && !Array.isArray(successSchemaRaw))
|
|
224
|
-
? successSchemaRaw
|
|
225
|
-
: undefined;
|
|
226
|
-
const addDirs = normalizeStringArray(job.add_dirs ?? job.addDirs);
|
|
227
|
-
const context = job.context != null ? String(job.context) : undefined;
|
|
228
|
-
const preCheck = job.pre_check != null ? String(job.pre_check) : undefined;
|
|
229
|
-
// ── Trick capabilities — symmetric with global parser ─────────
|
|
230
|
-
// (NB: this parser still lacks alwaysDeliver/attachments/
|
|
231
|
-
// requiresConfirmation/confirmationTimeoutMin from the global
|
|
232
|
-
// parser — pre-existing drift, fix in a separate change.)
|
|
233
|
-
const skills = normalizeStringArray(job.skills);
|
|
234
|
-
const allowedTools = normalizeStringArray(job.allowed_tools ?? job.allowedTools);
|
|
235
|
-
const allowedMcpServers = normalizeStringArray(job.allowed_mcp_servers ?? job.allowedMcpServers);
|
|
236
|
-
const tags = normalizeStringArray(job.tags);
|
|
237
|
-
const categoryRaw = job.category;
|
|
238
|
-
const category = typeof categoryRaw === 'string' && categoryRaw.trim()
|
|
239
|
-
? categoryRaw.trim().slice(0, 64)
|
|
240
|
-
: undefined;
|
|
241
|
-
const predictable = typeof job.predictable === 'boolean' ? job.predictable : undefined;
|
|
242
|
-
// 1.18.119 — symmetric with the global parseCronJobs description
|
|
243
|
-
// field. Without this, agent jobs always look "missing description"
|
|
244
|
-
// to the cron-clean migrator and stay flagged as eligible forever.
|
|
245
|
-
const description = typeof job.description === 'string' && job.description.trim()
|
|
246
|
-
? job.description.trim().slice(0, 500)
|
|
247
|
-
: undefined;
|
|
248
|
-
if (!name || !schedule || !prompt) {
|
|
214
|
+
const def = parseJobYaml(job);
|
|
215
|
+
if (!def) {
|
|
249
216
|
logger.warn({ job, agent: slug }, 'Skipping malformed agent cron job');
|
|
250
217
|
continue;
|
|
251
218
|
}
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
context, preCheck,
|
|
259
|
-
agentSlug: slug,
|
|
260
|
-
skills, allowedTools, allowedMcpServers, tags, category, predictable,
|
|
261
|
-
});
|
|
219
|
+
// Agent CRON.md stores BARE job names; we prefix with the slug at
|
|
220
|
+
// read time so the runtime can route by `<slug>:<job>` and the
|
|
221
|
+
// dashboard can disambiguate same-named jobs across agents.
|
|
222
|
+
// agentSlug is stamped from the folder location, overriding any
|
|
223
|
+
// value in the YAML — single source of truth.
|
|
224
|
+
allJobs.push({ ...def, name: `${slug}:${def.name}`, agentSlug: slug });
|
|
262
225
|
}
|
|
263
226
|
}
|
|
264
227
|
catch (err) {
|