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.
@@ -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(raw);
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 name = String(job.name ?? '');
102
- const schedule = String(job.schedule ?? '');
103
- const prompt = String(job.prompt ?? '');
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
- // 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 raw = readFileSync(cronFile, 'utf-8');
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 name = String(job.name ?? '');
201
- const schedule = String(job.schedule ?? '');
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
- // Prefix name with agent slug and tag with agentSlug
253
- allJobs.push({
254
- name: `${slug}:${name}`,
255
- schedule, prompt, enabled, tier, description, maxTurns, model, workDir,
256
- mode, maxHours, maxRetries, after,
257
- successCriteria, successCriteriaText, successSchema, addDirs,
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.122",
3
+ "version": "1.18.123",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",