clementine-agent 1.18.67 → 1.18.68
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/agent/run-agent-cron.d.ts +18 -1
- package/dist/agent/run-agent-cron.js +24 -8
- package/dist/cli/dashboard.js +51 -2
- package/dist/dashboard/build-operations.d.ts +4 -0
- package/dist/dashboard/build-operations.js +1 -0
- package/dist/gateway/cron-scheduler.js +7 -4
- package/dist/gateway/router.d.ts +3 -1
- package/dist/gateway/router.js +4 -1
- package/dist/tools/admin-tools.js +25 -6
- package/dist/types.d.ts +16 -0
- package/package.json +1 -1
|
@@ -62,9 +62,15 @@ export interface SkillContextResult {
|
|
|
62
62
|
* that don't resolve are surfaced via `missing[]` (warned, never fatal) so
|
|
63
63
|
* the dashboard can flag broken references.
|
|
64
64
|
*
|
|
65
|
+
* When `opts.skipAutoMatch` is true (predictable mode), only pinned skills
|
|
66
|
+
* load — the runtime keyword/semantic match is skipped entirely. The trick
|
|
67
|
+
* runs with ONLY the skills the user explicitly attached.
|
|
68
|
+
*
|
|
65
69
|
* Exported only for testability — the production caller is `runAgentCron`.
|
|
66
70
|
*/
|
|
67
|
-
export declare function buildSkillContext(jobName: string, jobPrompt: string, agentSlug: string | undefined, pinnedSkills: string[] | undefined, memoryStore?: MemoryStore | null
|
|
71
|
+
export declare function buildSkillContext(jobName: string, jobPrompt: string, agentSlug: string | undefined, pinnedSkills: string[] | undefined, memoryStore?: MemoryStore | null, opts?: {
|
|
72
|
+
skipAutoMatch?: boolean;
|
|
73
|
+
}): Promise<SkillContextResult>;
|
|
68
74
|
/** Minimal interface for the post-task reflection + skill extraction
|
|
69
75
|
* hooks. Lets `runAgentCron` stay decoupled from the full
|
|
70
76
|
* PersonalAssistant import while still benefiting from the existing
|
|
@@ -115,6 +121,13 @@ export interface RunAgentCronOptions {
|
|
|
115
121
|
* Applied after `buildExtraMcpForRunAgent` runs, so the effective set
|
|
116
122
|
* is `profile ∩ trick`. */
|
|
117
123
|
allowedMcpServers?: string[];
|
|
124
|
+
/** Predictable mode — when true, the runner skips the auto-injected
|
|
125
|
+
* context blocks (MEMORY.md, team comms, delegation queue) and the
|
|
126
|
+
* auto-matched skill search. The trick runs with ONLY what was
|
|
127
|
+
* explicitly attached: prompt, criteria, pinned skills, linked goals,
|
|
128
|
+
* prior progress. The fix for fire-time memory drift. Undefined =
|
|
129
|
+
* legacy behavior (inject everything). */
|
|
130
|
+
predictable?: boolean;
|
|
118
131
|
}
|
|
119
132
|
export interface RunAgentCronResult extends RunAgentResult {
|
|
120
133
|
/** The final prompt that was sent to the agent (after context injection).
|
|
@@ -167,6 +180,10 @@ export interface CronExecutionPlan {
|
|
|
167
180
|
maxBudgetUsd: number | undefined;
|
|
168
181
|
agentSlug: string | undefined;
|
|
169
182
|
ownerName: string;
|
|
183
|
+
/** Whether the trick is in predictable (contract) mode — true means
|
|
184
|
+
* MEMORY.md / team / delegation / auto-skills were intentionally
|
|
185
|
+
* skipped. Used by the Preview verdict line. */
|
|
186
|
+
predictable: boolean;
|
|
170
187
|
}
|
|
171
188
|
/**
|
|
172
189
|
* Plan a cron run — assemble all context, resolve skills, intersect tool/MCP
|
|
@@ -226,9 +226,13 @@ function buildCriteriaContext(successCriteria) {
|
|
|
226
226
|
* that don't resolve are surfaced via `missing[]` (warned, never fatal) so
|
|
227
227
|
* the dashboard can flag broken references.
|
|
228
228
|
*
|
|
229
|
+
* When `opts.skipAutoMatch` is true (predictable mode), only pinned skills
|
|
230
|
+
* load — the runtime keyword/semantic match is skipped entirely. The trick
|
|
231
|
+
* runs with ONLY the skills the user explicitly attached.
|
|
232
|
+
*
|
|
229
233
|
* Exported only for testability — the production caller is `runAgentCron`.
|
|
230
234
|
*/
|
|
231
|
-
export async function buildSkillContext(jobName, jobPrompt, agentSlug, pinnedSkills, memoryStore) {
|
|
235
|
+
export async function buildSkillContext(jobName, jobPrompt, agentSlug, pinnedSkills, memoryStore, opts) {
|
|
232
236
|
const applied = [];
|
|
233
237
|
const missing = [];
|
|
234
238
|
try {
|
|
@@ -259,8 +263,10 @@ export async function buildSkillContext(jobName, jobPrompt, agentSlug, pinnedSki
|
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
265
|
// 2. Auto-match fills the remainder, deduped against pins.
|
|
266
|
+
// In predictable (contract) mode we skip this entirely — only
|
|
267
|
+
// pinned skills load, the runtime keyword/semantic search is off.
|
|
262
268
|
const remaining = MAX_INJECTED_SKILLS - prepared.length;
|
|
263
|
-
if (remaining > 0) {
|
|
269
|
+
if (remaining > 0 && !opts?.skipAutoMatch) {
|
|
264
270
|
const matched = searchSkills(skillQuery, remaining + (pinnedSkills?.length ?? 0), agentSlug, { suppressedNames });
|
|
265
271
|
for (const m of matched) {
|
|
266
272
|
if (prepared.length >= MAX_INJECTED_SKILLS)
|
|
@@ -320,13 +326,22 @@ export async function buildCronExecutionPlan(opts) {
|
|
|
320
326
|
const tier = opts.tier ?? 1;
|
|
321
327
|
const agentSlug = opts.profile?.slug;
|
|
322
328
|
const ownerName = process.env.OWNER_NAME ?? 'the user';
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
329
|
+
// ── Predictable (contract) mode ────────────────────────────────────
|
|
330
|
+
// When `predictable: true`, the trick runs with ONLY what was explicitly
|
|
331
|
+
// attached — prompt, criteria, pinned skills, linked goals, prior progress.
|
|
332
|
+
// We skip MEMORY.md, team comms, delegation queue, and the runtime skill
|
|
333
|
+
// auto-match. This is the fix for the email-cadence failure mode where the
|
|
334
|
+
// agent agreed to a plan in chat then re-derived from drifted memory at
|
|
335
|
+
// fire time. Legacy tricks (predictable === undefined) preserve existing
|
|
336
|
+
// behavior so we don't surprise anyone.
|
|
337
|
+
const predictable = opts.predictable === true;
|
|
338
|
+
const memoryContext = predictable ? '' : buildAutonomousMemoryContext(opts.profile);
|
|
339
|
+
const progressContext = buildProgressContext(opts.jobName); // opt-in via cron_progress writes
|
|
340
|
+
const goalContext = buildGoalContext(opts.jobName); // explicit links; not auto-inferred
|
|
341
|
+
const delegationContext = predictable ? '' : buildDelegationContext(agentSlug);
|
|
342
|
+
const teamContext = predictable ? '' : buildTeamContext(agentSlug);
|
|
328
343
|
const criteriaContext = buildCriteriaContext(opts.successCriteria);
|
|
329
|
-
const skillResult = await buildSkillContext(opts.jobName, opts.jobPrompt, agentSlug, opts.pinnedSkills, opts.memoryStore);
|
|
344
|
+
const skillResult = await buildSkillContext(opts.jobName, opts.jobPrompt, agentSlug, opts.pinnedSkills, opts.memoryStore, { skipAutoMatch: predictable });
|
|
330
345
|
const skillContext = skillResult.text;
|
|
331
346
|
const howToRespond = `## How to respond\n` +
|
|
332
347
|
`You're sending this directly to ${ownerName} as a DM. ` +
|
|
@@ -390,6 +405,7 @@ export async function buildCronExecutionPlan(opts) {
|
|
|
390
405
|
maxBudgetUsd: maxBudget,
|
|
391
406
|
agentSlug,
|
|
392
407
|
ownerName,
|
|
408
|
+
predictable,
|
|
393
409
|
};
|
|
394
410
|
}
|
|
395
411
|
/**
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -6237,7 +6237,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6237
6237
|
// ── CRON CRUD routes (continued) ──────────────────────────────
|
|
6238
6238
|
app.post('/api/cron', (req, res) => {
|
|
6239
6239
|
try {
|
|
6240
|
-
const { name, schedule, prompt, tier, enabled, work_dir, mode, max_hours, max_retries, after, agent, context, skills, allowedTools, allowedMcpServers, tags, category, } = req.body;
|
|
6240
|
+
const { name, schedule, prompt, tier, enabled, work_dir, mode, max_hours, max_retries, after, agent, context, skills, allowedTools, allowedMcpServers, tags, category, predictable, } = req.body;
|
|
6241
6241
|
if (!name || !schedule || !prompt) {
|
|
6242
6242
|
res.status(400).json({ error: 'name, schedule, and prompt are required' });
|
|
6243
6243
|
return;
|
|
@@ -6293,6 +6293,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6293
6293
|
if (typeof category === 'string' && category.trim()) {
|
|
6294
6294
|
job.category = category.trim().slice(0, 64);
|
|
6295
6295
|
}
|
|
6296
|
+
// Predictable mode — default to true (contract execution) for new
|
|
6297
|
+
// tricks created via the dashboard. Mirror the MCP tool default.
|
|
6298
|
+
job.predictable = (predictable === false) ? false : true;
|
|
6296
6299
|
jobs.push(job);
|
|
6297
6300
|
writeCronFileAt(cronFile, parsed, jobs);
|
|
6298
6301
|
res.json({ ok: true, message: `Created cron job: ${name}` });
|
|
@@ -6431,6 +6434,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6431
6434
|
delete jobs[idx].category;
|
|
6432
6435
|
}
|
|
6433
6436
|
}
|
|
6437
|
+
if (updates.predictable !== undefined) {
|
|
6438
|
+
jobs[idx].predictable = Boolean(updates.predictable);
|
|
6439
|
+
}
|
|
6434
6440
|
if (updates.name !== undefined && updates.name !== bareJobName) {
|
|
6435
6441
|
// Rename — check for duplicates
|
|
6436
6442
|
const dup = jobs.find((j, i) => i !== idx && String(j.name ?? '').toLowerCase() === String(updates.name).toLowerCase());
|
|
@@ -6548,6 +6554,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6548
6554
|
pinnedSkills: job.skills,
|
|
6549
6555
|
allowedTools: job.allowedTools,
|
|
6550
6556
|
allowedMcpServers: job.allowedMcpServers,
|
|
6557
|
+
predictable: job.predictable,
|
|
6551
6558
|
});
|
|
6552
6559
|
// Enrich each applied skill with its title/description/full markdown
|
|
6553
6560
|
// body so the UI can render "what the agent will actually read".
|
|
@@ -6586,7 +6593,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6586
6593
|
mode: job.mode ?? null,
|
|
6587
6594
|
tags: job.tags ?? [],
|
|
6588
6595
|
category: job.category ?? null,
|
|
6596
|
+
predictable: typeof job.predictable === 'boolean' ? job.predictable : null,
|
|
6589
6597
|
},
|
|
6598
|
+
predictable: plan.predictable,
|
|
6590
6599
|
profile: profile ? { slug: profile.slug, name: profile.name } : null,
|
|
6591
6600
|
builtPrompt: plan.builtPrompt,
|
|
6592
6601
|
contextBlocks: plan.contextBlocks,
|
|
@@ -19673,6 +19682,19 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19673
19682
|
<div class="form-group">
|
|
19674
19683
|
<label class="form-label">Capabilities <span style="color:var(--text-muted);font-weight:normal">(optional — pin skills + scope tools/MCP)</span></label>
|
|
19675
19684
|
<div class="form-hint" style="margin-bottom:6px">Pin learned procedures and constrain which tools / MCP servers this trick can use. Empty = inherit defaults. Use Preview on the card to see exactly what gets sent.</div>
|
|
19685
|
+
<div class="cap-section">
|
|
19686
|
+
<label class="cap-section-label">Predictable Mode</label>
|
|
19687
|
+
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer;font-size:12px;color:var(--text-primary)">
|
|
19688
|
+
<input type="checkbox" id="cron-predictable" checked style="margin-top:3px">
|
|
19689
|
+
<span>
|
|
19690
|
+
<strong>Run with only what's attached</strong> (recommended)
|
|
19691
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:3px;line-height:1.5">
|
|
19692
|
+
ON: MEMORY.md, team comms, delegation queue, and auto-matched skills are SKIPPED at fire-time. The trick runs ONLY with the prompt + pinned skills + tools you see here. No drift, no surprise.<br>
|
|
19693
|
+
OFF: legacy mode — runner injects MEMORY.md and other live context. Use only when the trick legitimately needs to re-read memory each fire (e.g. "summarize yesterday's daily note").
|
|
19694
|
+
</div>
|
|
19695
|
+
</span>
|
|
19696
|
+
</label>
|
|
19697
|
+
</div>
|
|
19676
19698
|
<div class="cap-section">
|
|
19677
19699
|
<label class="cap-section-label">Pinned Skills</label>
|
|
19678
19700
|
<div class="cap-picker-chips" id="cron-skills-chips"></div>
|
|
@@ -22726,6 +22748,8 @@ function renderScheduledTaskCard(task) {
|
|
|
22726
22748
|
var badges = '';
|
|
22727
22749
|
if (task.owner) badges += '<span class="badge badge-orange">' + esc(task.owner) + '</span>';
|
|
22728
22750
|
if (task.category) badges += '<span class="badge badge-gray" title="Category">' + esc(task.category) + '</span>';
|
|
22751
|
+
if (task.predictable === true) badges += '<span class="badge badge-green" title="Contract mode — runs with only the prompt + pinned skills/tools. No MEMORY.md, no auto-matched skills, no team comms injection at fire-time.">🔒 predictable</span>';
|
|
22752
|
+
else if (task.predictable === false) badges += '<span class="badge badge-yellow" title="Dynamic mode — fire-time injects MEMORY.md, recent team activity, and auto-matched skills. Can drift from chat-time intent.">🔄 reads memory</span>';
|
|
22729
22753
|
if (task.mode === 'unleashed') badges += '<span class="badge badge-purple">long-running</span>';
|
|
22730
22754
|
if (task.after) badges += '<span class="badge badge-yellow" title="Triggered after ' + esc(task.after) + '">after ' + esc(task.after) + '</span>';
|
|
22731
22755
|
if (task.maxRetries != null) badges += '<span class="badge badge-gray">' + esc(task.maxRetries) + ' retries</span>';
|
|
@@ -23789,6 +23813,9 @@ function resetTrickCapabilityState() {
|
|
|
23789
23813
|
if (toolsToggle) toolsToggle.textContent = '▾ Show';
|
|
23790
23814
|
var catEl = document.getElementById('cron-category');
|
|
23791
23815
|
if (catEl) catEl.value = '';
|
|
23816
|
+
// Default: predictable ON for new tricks (matches add_cron_job default).
|
|
23817
|
+
var predEl = document.getElementById('cron-predictable');
|
|
23818
|
+
if (predEl) predEl.checked = true;
|
|
23792
23819
|
renderSkillsPickerChips();
|
|
23793
23820
|
renderMcpPickerChips();
|
|
23794
23821
|
renderTagsPickerChips();
|
|
@@ -24102,6 +24129,10 @@ function openEditCronModal(jobName) {
|
|
|
24102
24129
|
if (allowedTools.length > 0) toggleAllowedToolsPanel();
|
|
24103
24130
|
var catEl = document.getElementById('cron-category');
|
|
24104
24131
|
if (catEl) catEl.value = job.category || '';
|
|
24132
|
+
// Predictable: respect saved value; if undefined (legacy trick), keep
|
|
24133
|
+
// unchecked so we don't silently change runner behavior.
|
|
24134
|
+
var predEl = document.getElementById('cron-predictable');
|
|
24135
|
+
if (predEl) predEl.checked = (job.predictable === true);
|
|
24105
24136
|
renderSkillsPickerChips();
|
|
24106
24137
|
renderMcpPickerChips();
|
|
24107
24138
|
renderTagsPickerChips();
|
|
@@ -24140,7 +24171,23 @@ function closeCronPreviewModal() {
|
|
|
24140
24171
|
|
|
24141
24172
|
function renderCronPreview(d) {
|
|
24142
24173
|
var html = '';
|
|
24143
|
-
//
|
|
24174
|
+
// Predictable verdict line — the headline visibility win.
|
|
24175
|
+
html += '<div class="preview-section">';
|
|
24176
|
+
if (d.predictable === true) {
|
|
24177
|
+
html += '<div style="padding:10px 12px;border-radius:6px;background:rgba(16,185,129,0.12);color:var(--green);font-size:13px;font-weight:500">'
|
|
24178
|
+
+ '🔒 <strong>Predictable</strong> — what you see here is exactly what will run. No MEMORY.md, no team comms, no auto-matched skills injected at fire-time.'
|
|
24179
|
+
+ '</div>';
|
|
24180
|
+
} else if (d.predictable === false) {
|
|
24181
|
+
html += '<div style="padding:10px 12px;border-radius:6px;background:rgba(245,158,11,0.12);color:var(--yellow);font-size:13px;font-weight:500">'
|
|
24182
|
+
+ '⚠ <strong>Reads memory at fire-time</strong> — fire-time will ALSO inject MEMORY.md, recent team comms, delegation queue, and auto-matched skills. Output may differ from this preview if those drift between now and fire.'
|
|
24183
|
+
+ '</div>';
|
|
24184
|
+
} else {
|
|
24185
|
+
html += '<div style="padding:10px 12px;border-radius:6px;background:var(--bg-tertiary);color:var(--text-muted);font-size:12px">'
|
|
24186
|
+
+ 'Legacy trick — predictable mode not set. Runs in dynamic mode (injects MEMORY.md, etc). Edit and turn on Predictable Mode to lock down behavior.'
|
|
24187
|
+
+ '</div>';
|
|
24188
|
+
}
|
|
24189
|
+
html += '</div>';
|
|
24190
|
+
// Warnings band
|
|
24144
24191
|
if (Array.isArray(d.warnings) && d.warnings.length > 0) {
|
|
24145
24192
|
html += '<div class="preview-section">';
|
|
24146
24193
|
for (var w = 0; w < d.warnings.length; w++) {
|
|
@@ -24341,6 +24388,7 @@ async function saveCronJob() {
|
|
|
24341
24388
|
const categoryRaw = (document.getElementById('cron-category')?.value || '').trim();
|
|
24342
24389
|
const category = categoryRaw || undefined;
|
|
24343
24390
|
const allowedTools = parseAllowedToolsRaw();
|
|
24391
|
+
const predictable = !!document.getElementById('cron-predictable')?.checked;
|
|
24344
24392
|
|
|
24345
24393
|
if (!name || !schedule || !prompt) {
|
|
24346
24394
|
toast('Please fill in all fields', 'error');
|
|
@@ -24363,6 +24411,7 @@ async function saveCronJob() {
|
|
|
24363
24411
|
: (_cronSelectedMcp.length ? _cronSelectedMcp : undefined),
|
|
24364
24412
|
tags: editingCronJob ? _cronTags : (_cronTags.length ? _cronTags : undefined),
|
|
24365
24413
|
category: editingCronJob ? (category || '') : category,
|
|
24414
|
+
predictable,
|
|
24366
24415
|
};
|
|
24367
24416
|
|
|
24368
24417
|
if (editingCronJob) {
|
|
@@ -97,6 +97,10 @@ export interface ScheduledTaskCard {
|
|
|
97
97
|
tags?: string[];
|
|
98
98
|
/** Optional category bucket. */
|
|
99
99
|
category?: string;
|
|
100
|
+
/** Predictable (contract) mode — true means runner skips MEMORY.md /
|
|
101
|
+
* team comms / auto-matched skills. The visibility-on-card flag for
|
|
102
|
+
* "this trick will run with only what you see here." */
|
|
103
|
+
predictable?: boolean;
|
|
100
104
|
}
|
|
101
105
|
export interface ScheduledWorkflowCard {
|
|
102
106
|
type: 'scheduled_workflow';
|
|
@@ -215,6 +215,7 @@ export function buildOperationsSnapshot(input) {
|
|
|
215
215
|
allowedMcpServers: asStringArray(job.allowed_mcp_servers ?? job.allowedMcpServers),
|
|
216
216
|
tags: asStringArray(job.tags),
|
|
217
217
|
category: typeof job.category === 'string' && job.category.trim() ? job.category.trim() : undefined,
|
|
218
|
+
predictable: typeof job.predictable === 'boolean' ? job.predictable : undefined,
|
|
218
219
|
};
|
|
219
220
|
}).sort((a, b) => a.owner.localeCompare(b.owner) || a.displayName.localeCompare(b.displayName));
|
|
220
221
|
const scheduledWorkflows = input.workflowSummaries
|
|
@@ -132,6 +132,8 @@ export function parseCronJobs() {
|
|
|
132
132
|
const category = typeof categoryRaw === 'string' && categoryRaw.trim()
|
|
133
133
|
? categoryRaw.trim().slice(0, 64)
|
|
134
134
|
: undefined;
|
|
135
|
+
// Predictable (contract) mode — undefined means legacy behavior.
|
|
136
|
+
const predictable = typeof job.predictable === 'boolean' ? job.predictable : undefined;
|
|
135
137
|
if (!name || !schedule || !prompt) {
|
|
136
138
|
logger.warn({ job }, 'Skipping malformed cron job');
|
|
137
139
|
continue;
|
|
@@ -139,7 +141,7 @@ export function parseCronJobs() {
|
|
|
139
141
|
jobs.push({
|
|
140
142
|
name, schedule, prompt, enabled, tier, maxTurns, model, workDir, mode,
|
|
141
143
|
maxHours, maxRetries, after, successCriteria, alwaysDeliver, context, preCheck, agentSlug,
|
|
142
|
-
skills, allowedTools, allowedMcpServers, tags, category,
|
|
144
|
+
skills, allowedTools, allowedMcpServers, tags, category, predictable,
|
|
143
145
|
});
|
|
144
146
|
}
|
|
145
147
|
return jobs;
|
|
@@ -199,6 +201,7 @@ export function parseAgentCronJobs(agentsDir) {
|
|
|
199
201
|
const category = typeof categoryRaw === 'string' && categoryRaw.trim()
|
|
200
202
|
? categoryRaw.trim().slice(0, 64)
|
|
201
203
|
: undefined;
|
|
204
|
+
const predictable = typeof job.predictable === 'boolean' ? job.predictable : undefined;
|
|
202
205
|
if (!name || !schedule || !prompt) {
|
|
203
206
|
logger.warn({ job, agent: slug }, 'Skipping malformed agent cron job');
|
|
204
207
|
continue;
|
|
@@ -209,7 +212,7 @@ export function parseAgentCronJobs(agentsDir) {
|
|
|
209
212
|
schedule, prompt, enabled, tier, maxTurns, model, workDir,
|
|
210
213
|
mode, maxHours, maxRetries, after, successCriteria, context, preCheck,
|
|
211
214
|
agentSlug: slug,
|
|
212
|
-
skills, allowedTools, allowedMcpServers, tags, category,
|
|
215
|
+
skills, allowedTools, allowedMcpServers, tags, category, predictable,
|
|
213
216
|
});
|
|
214
217
|
}
|
|
215
218
|
}
|
|
@@ -1094,12 +1097,12 @@ export class CronScheduler {
|
|
|
1094
1097
|
const startedAt = new Date();
|
|
1095
1098
|
try {
|
|
1096
1099
|
// Standard cron jobs get a timeout via SDK AbortController (advisor may override)
|
|
1097
|
-
let response = await this.gateway.handleCronJob(job.name, jobPrompt, job.tier, job.maxTurns, job.model, job.workDir, job.mode, job.maxHours, effectiveTimeoutMs, job.successCriteria, job.agentSlug, job.skills, job.allowedTools, job.allowedMcpServers);
|
|
1100
|
+
let response = await this.gateway.handleCronJob(job.name, jobPrompt, job.tier, job.maxTurns, job.model, job.workDir, job.mode, job.maxHours, effectiveTimeoutMs, job.successCriteria, job.agentSlug, job.skills, job.allowedTools, job.allowedMcpServers, job.predictable);
|
|
1098
1101
|
// alwaysDeliver: retry once if the response is empty/noise
|
|
1099
1102
|
if (job.alwaysDeliver && (!response || CronScheduler.isCronNoise(response))) {
|
|
1100
1103
|
logger.info({ job: job.name }, 'alwaysDeliver: empty/noise response — retrying once');
|
|
1101
1104
|
try {
|
|
1102
|
-
const retryResponse = await this.gateway.handleCronJob(job.name, jobPrompt + '\n\nYou MUST produce a brief status update. Do NOT return __NOTHING__.', job.tier, job.maxTurns, job.model, job.workDir, job.mode, job.maxHours, effectiveTimeoutMs, job.successCriteria, job.agentSlug, job.skills, job.allowedTools, job.allowedMcpServers);
|
|
1105
|
+
const retryResponse = await this.gateway.handleCronJob(job.name, jobPrompt + '\n\nYou MUST produce a brief status update. Do NOT return __NOTHING__.', job.tier, job.maxTurns, job.model, job.workDir, job.mode, job.maxHours, effectiveTimeoutMs, job.successCriteria, job.agentSlug, job.skills, job.allowedTools, job.allowedMcpServers, job.predictable);
|
|
1103
1106
|
if (retryResponse && !CronScheduler.isCronNoise(retryResponse)) {
|
|
1104
1107
|
response = retryResponse;
|
|
1105
1108
|
}
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -175,7 +175,9 @@ export declare class Gateway {
|
|
|
175
175
|
handleCronJob(jobName: string, jobPrompt: string, tier?: number, maxTurns?: number, model?: string, workDir?: string,
|
|
176
176
|
/** Accepted for back-compat; canonical SDK path executes every job
|
|
177
177
|
* identically. Affects only UI display + budget heuristics elsewhere. */
|
|
178
|
-
_mode?: 'standard' | 'unleashed', maxHours?: number, timeoutMs?: number, successCriteria?: string[], agentSlug?: string, pinnedSkills?: string[], allowedTools?: string[], allowedMcpServers?: string[]
|
|
178
|
+
_mode?: 'standard' | 'unleashed', maxHours?: number, timeoutMs?: number, successCriteria?: string[], agentSlug?: string, pinnedSkills?: string[], allowedTools?: string[], allowedMcpServers?: string[],
|
|
179
|
+
/** Predictable (contract) mode — runner skips memory/team/auto-skills. */
|
|
180
|
+
predictable?: boolean): Promise<string>;
|
|
179
181
|
/**
|
|
180
182
|
* Process a team message as an autonomous task — same multi-phase execution
|
|
181
183
|
* as cron unleashed jobs, so agents can work until done instead of being
|
package/dist/gateway/router.js
CHANGED
|
@@ -1969,7 +1969,9 @@ export class Gateway {
|
|
|
1969
1969
|
* identically. Affects only UI display + budget heuristics elsewhere. */
|
|
1970
1970
|
_mode, maxHours, timeoutMs, successCriteria, agentSlug,
|
|
1971
1971
|
// ── Trick capabilities (optional; preserve today's behavior when omitted) ─
|
|
1972
|
-
pinnedSkills, allowedTools, allowedMcpServers
|
|
1972
|
+
pinnedSkills, allowedTools, allowedMcpServers,
|
|
1973
|
+
/** Predictable (contract) mode — runner skips memory/team/auto-skills. */
|
|
1974
|
+
predictable) {
|
|
1973
1975
|
const releaseLane = await lanes.acquire('cron');
|
|
1974
1976
|
// Build a wall-clock abort timer from maxHours / timeoutMs.
|
|
1975
1977
|
// Whichever is shorter wins. Defaults to 1h if neither is set.
|
|
@@ -2010,6 +2012,7 @@ export class Gateway {
|
|
|
2010
2012
|
pinnedSkills,
|
|
2011
2013
|
allowedTools,
|
|
2012
2014
|
allowedMcpServers,
|
|
2015
|
+
predictable,
|
|
2013
2016
|
});
|
|
2014
2017
|
scanner.refreshIntegrity();
|
|
2015
2018
|
// Stash trick-capability metadata for the scheduler to read when
|
|
@@ -1025,20 +1025,21 @@ export function registerAdminTools(server) {
|
|
|
1025
1025
|
return textResult(lines.join('\n\n'));
|
|
1026
1026
|
});
|
|
1027
1027
|
// ── Add Cron Job ────────────────────────────────────────────────────────
|
|
1028
|
-
server.tool('add_cron_job', 'Add a new scheduled
|
|
1028
|
+
server.tool('add_cron_job', 'Add a new scheduled task. ⚠ BEFORE CALLING THIS TOOL: propose the concrete plan to the user in chat and get explicit approval. The `prompt` you save should be SELF-CONTAINED — list the actual recipients, the actual template/content, the actual criteria. AVOID vague references like "recent leads" or "this week\'s items" that the trick will re-derive at fire-time, because re-derivation reads from MEMORY.md which drifts between chat-time agreement and fire-time execution. Good prompt: "Send template `monday-followup` to alice@x.com, bob@y.com, carol@z.com." Bad prompt: "Send follow-up to recent leads." The default `predictable: true` mode runs the trick with ONLY the prompt + explicitly-attached skills/tools — no MEMORY.md, no team-comms injection, no runtime skill auto-match. Set `predictable: false` ONLY if the user explicitly wants a dynamic trick that re-resolves data each fire (e.g., "summarize yesterday\'s daily note" where the data legitimately changes).', {
|
|
1029
1029
|
name: z.string().describe('Job name (unique identifier)'),
|
|
1030
1030
|
schedule: z.string().describe('Cron expression (e.g., "0 9 * * 1" for Monday 9 AM)'),
|
|
1031
|
-
prompt: z.string().describe('The prompt/instruction for the assistant to execute'),
|
|
1031
|
+
prompt: z.string().describe('The prompt/instruction for the assistant to execute. SHOULD BE CONCRETE — list actual recipients, criteria, content. Vague prompts re-derive at fire-time and cause "agent agreed in chat but emailed wrong people" failures.'),
|
|
1032
1032
|
tier: z.number().optional().default(1).describe('Security tier (1=auto, 2=logged, 3=approval). Tier 2+ also raises the per-run budget cap.'),
|
|
1033
1033
|
enabled: z.boolean().optional().default(true).describe('Whether the job is enabled'),
|
|
1034
1034
|
work_dir: z.string().optional().describe('Project directory to run in (agent gets access to project tools, CLAUDE.md, files)'),
|
|
1035
1035
|
max_hours: z.number().optional().describe('Wall-clock cap in hours. Defaults to 1h. Run aborts via AbortSignal when exceeded.'),
|
|
1036
|
-
|
|
1036
|
+
predictable: z.boolean().optional().default(true).describe('PREDICTABLE MODE (default true, recommended). When true, the runner runs with ONLY the prompt + pinned skills + criteria + linked goals + prior progress — MEMORY.md, team comms, delegation queue, and runtime skill auto-match are SKIPPED. This is the contract model: trick executes the plan you saved, not whatever memory says today. Set to false only when the user EXPLICITLY needs dynamic behavior — and tell them what that means.'),
|
|
1037
|
+
skills: z.array(z.string()).optional().describe('Pinned skill slugs (filename minus .md, slashes flattened to dashes). Loaded BEFORE runtime auto-match. Total injected per run capped at 4. In predictable mode, ONLY pinned skills load (no auto-match).'),
|
|
1037
1038
|
allowed_tools: z.array(z.string()).optional().describe('Per-trick tool whitelist. When set, intersected with the agent profile allowlist. Agent is always force-included for sub-agent delegation. Empty/omitted inherits from profile.'),
|
|
1038
1039
|
allowed_mcp_servers: z.array(z.string()).optional().describe('Per-trick MCP server whitelist (server names from list_mcp_servers). Applied AFTER profile allowlist. Empty/omitted inherits from profile.'),
|
|
1039
1040
|
tags: z.array(z.string()).optional().describe('Free-form tags for grouping/filtering in the dashboard.'),
|
|
1040
1041
|
category: z.string().optional().describe('Single category bucket (e.g. "ops", "research").'),
|
|
1041
|
-
}, async ({ name: jobName, schedule, prompt, tier, enabled, work_dir, max_hours, skills, allowed_tools, allowed_mcp_servers, tags, category }) => {
|
|
1042
|
+
}, async ({ name: jobName, schedule, prompt, tier, enabled, work_dir, max_hours, predictable, skills, allowed_tools, allowed_mcp_servers, tags, category }) => {
|
|
1042
1043
|
// Validate cron expression
|
|
1043
1044
|
const cronMod = await import('node-cron');
|
|
1044
1045
|
if (!cronMod.default.validate(schedule)) {
|
|
@@ -1076,6 +1077,9 @@ export function registerAdminTools(server) {
|
|
|
1076
1077
|
newJob.work_dir = work_dir;
|
|
1077
1078
|
if (max_hours)
|
|
1078
1079
|
newJob.max_hours = max_hours;
|
|
1080
|
+
// Predictable mode: persist explicitly so behavior is locked. Default
|
|
1081
|
+
// for new chat-created tricks is true (contract execution).
|
|
1082
|
+
newJob.predictable = predictable !== false;
|
|
1079
1083
|
// ── Trick capabilities (snake_case YAML keys) ──────────────────
|
|
1080
1084
|
if (Array.isArray(skills) && skills.length)
|
|
1081
1085
|
newJob.skills = skills.map(s => String(s).trim()).filter(Boolean);
|
|
@@ -1121,6 +1125,7 @@ export function registerAdminTools(server) {
|
|
|
1121
1125
|
details.push(` Project: ${work_dir}`);
|
|
1122
1126
|
if (max_hours)
|
|
1123
1127
|
details.push(` Wall-clock cap: ${max_hours}h`);
|
|
1128
|
+
details.push(` Predictable mode: ${newJob.predictable ? 'ON — runs with only the prompt + pinned skills/tools (no MEMORY.md drift)' : 'OFF — runs with MEMORY.md + auto-matched skills (dynamic, may surprise)'}`);
|
|
1124
1129
|
if (Array.isArray(skills) && skills.length)
|
|
1125
1130
|
details.push(` Pinned skills: ${skills.join(', ')}`);
|
|
1126
1131
|
if (Array.isArray(allowed_tools) && allowed_tools.length)
|
|
@@ -1138,7 +1143,7 @@ export function registerAdminTools(server) {
|
|
|
1138
1143
|
return textResult(`Added cron job "${jobName}":\n${details.join('\n')}\n\n${verifyMsg}${goalHint}`);
|
|
1139
1144
|
});
|
|
1140
1145
|
// ── Update Cron Job ─────────────────────────────────────────────────────
|
|
1141
|
-
server.tool('update_cron_job', 'Update an existing cron job in CRON.md. Partial — only fields you supply change. To CLEAR a capability allowlist (skills/allowed_tools/allowed_mcp_servers/tags), pass an empty array. To clear category, pass an empty string. The daemon auto-reloads on file change. Use preview_cron_job to confirm what will run before the next fire.', {
|
|
1146
|
+
server.tool('update_cron_job', 'Update an existing cron job in CRON.md. Partial — only fields you supply change. To CLEAR a capability allowlist (skills/allowed_tools/allowed_mcp_servers/tags), pass an empty array. To clear category, pass an empty string. The daemon auto-reloads on file change. Use preview_cron_job to confirm what will run before the next fire. ⚠ Flipping `predictable` from true to false changes whether the trick reads MEMORY.md at fire-time — make sure the user understands the tradeoff before you toggle it.', {
|
|
1142
1147
|
name: z.string().describe('Existing job name to update.'),
|
|
1143
1148
|
schedule: z.string().optional().describe('New cron expression.'),
|
|
1144
1149
|
prompt: z.string().optional().describe('New prompt.'),
|
|
@@ -1146,12 +1151,13 @@ export function registerAdminTools(server) {
|
|
|
1146
1151
|
enabled: z.boolean().optional().describe('Enable/disable.'),
|
|
1147
1152
|
work_dir: z.string().optional().describe('Project directory. Empty string clears.'),
|
|
1148
1153
|
max_hours: z.number().optional().describe('Wall-clock cap in hours.'),
|
|
1154
|
+
predictable: z.boolean().optional().describe('Predictable (contract) mode. true = runner skips MEMORY.md / team comms / auto-matched skills, runs ONLY with the prompt + pinned items. false = legacy injects-everything mode (memory drift risk). Tell the user what they\'re opting into when flipping this.'),
|
|
1149
1155
|
skills: z.array(z.string()).optional().describe('Pinned skill slugs. Empty array clears.'),
|
|
1150
1156
|
allowed_tools: z.array(z.string()).optional().describe('Tool allowlist. Empty array clears.'),
|
|
1151
1157
|
allowed_mcp_servers: z.array(z.string()).optional().describe('MCP allowlist. Empty array clears.'),
|
|
1152
1158
|
tags: z.array(z.string()).optional().describe('Tags. Empty array clears.'),
|
|
1153
1159
|
category: z.string().optional().describe('Category bucket. Empty string clears.'),
|
|
1154
|
-
}, async ({ name: jobName, schedule, prompt, tier, enabled, work_dir, max_hours, skills, allowed_tools, allowed_mcp_servers, tags, category }) => {
|
|
1160
|
+
}, async ({ name: jobName, schedule, prompt, tier, enabled, work_dir, max_hours, predictable, skills, allowed_tools, allowed_mcp_servers, tags, category }) => {
|
|
1155
1161
|
if (!existsSync(CRON_FILE)) {
|
|
1156
1162
|
return textResult('CRON.md not found. Use add_cron_job to create one first.');
|
|
1157
1163
|
}
|
|
@@ -1202,6 +1208,10 @@ export function registerAdminTools(server) {
|
|
|
1202
1208
|
job.max_hours = max_hours;
|
|
1203
1209
|
changed.push(`max_hours → ${max_hours}`);
|
|
1204
1210
|
}
|
|
1211
|
+
if (predictable !== undefined) {
|
|
1212
|
+
job.predictable = predictable;
|
|
1213
|
+
changed.push(`predictable → ${predictable ? 'ON (contract mode — only what\'s attached)' : 'OFF (dynamic — reads MEMORY.md, may drift)'}`);
|
|
1214
|
+
}
|
|
1205
1215
|
// ── Capabilities — empty array CLEARS, omitted leaves alone ────
|
|
1206
1216
|
if (skills !== undefined) {
|
|
1207
1217
|
if (skills.length) {
|
|
@@ -1298,11 +1308,20 @@ export function registerAdminTools(server) {
|
|
|
1298
1308
|
pinnedSkills: job.skills,
|
|
1299
1309
|
allowedTools: job.allowedTools,
|
|
1300
1310
|
allowedMcpServers: job.allowedMcpServers,
|
|
1311
|
+
predictable: job.predictable,
|
|
1301
1312
|
});
|
|
1302
1313
|
const allServers = discoverMcpServers();
|
|
1303
1314
|
const lines = [];
|
|
1304
1315
|
lines.push(`# Preview: ${job.name}`);
|
|
1305
1316
|
lines.push('');
|
|
1317
|
+
// Verdict line — the headline visibility win for the user.
|
|
1318
|
+
if (plan.predictable) {
|
|
1319
|
+
lines.push(`✓ **Predictable** — what you see here is exactly what will run. No MEMORY.md, no team activity, no auto-matched skills injected at fire-time. Pure contract execution.`);
|
|
1320
|
+
}
|
|
1321
|
+
else {
|
|
1322
|
+
lines.push(`⚠ **Reads memory at fire-time** — this trick is in dynamic mode. At fire-time, the runner ALSO injects MEMORY.md, recent team comms, delegation queue, and auto-matched skills. The agent's output may differ from this preview if those have drifted since chat-time. Set \`predictable: true\` if that's not what you want.`);
|
|
1323
|
+
}
|
|
1324
|
+
lines.push('');
|
|
1306
1325
|
lines.push(`**Schedule:** ${job.schedule} **Tier:** ${plan.tier} (${plan.effort}${plan.maxBudgetUsd ? `, budget $${plan.maxBudgetUsd}` : ''})`);
|
|
1307
1326
|
if (job.agentSlug)
|
|
1308
1327
|
lines.push(`**Agent:** ${job.agentSlug}`);
|
package/dist/types.d.ts
CHANGED
|
@@ -354,6 +354,22 @@ export interface CronJobDefinition {
|
|
|
354
354
|
/** Single category bucket — convenience for default grouping in the
|
|
355
355
|
* dashboard (e.g. "ops", "research", "morning"). */
|
|
356
356
|
category?: string;
|
|
357
|
+
/**
|
|
358
|
+
* Predictable mode (the "contract" model) — runs the trick with ONLY
|
|
359
|
+
* the prompt + explicitly-attached skills/criteria/goals + tools. Skips
|
|
360
|
+
* MEMORY.md injection, auto-matched skills, team comms, and delegation
|
|
361
|
+
* queue. The fix for "agent said OK in chat then fired with stale memory."
|
|
362
|
+
*
|
|
363
|
+
* - undefined / false: legacy behavior — runner injects everything
|
|
364
|
+
* (MEMORY.md, auto-matched skills, team activity, delegation). What
|
|
365
|
+
* chat-style autonomous work needs, but contaminates scheduled tasks.
|
|
366
|
+
* - true: contract mode — runner only includes what was explicitly
|
|
367
|
+
* attached. The trick executes the plan you saw in chat, nothing more.
|
|
368
|
+
*
|
|
369
|
+
* `add_cron_job` defaults this to `true` for new chat-created tricks.
|
|
370
|
+
* Existing tricks (no field set) keep current behavior — backward compat.
|
|
371
|
+
*/
|
|
372
|
+
predictable?: boolean;
|
|
357
373
|
}
|
|
358
374
|
export type LongTaskRisk = 'normal' | 'long' | 'huge' | 'unsafe';
|
|
359
375
|
export type LongTaskRoute = 'standard' | 'checkpointed' | 'opus_1m' | 'sonnet_1m' | 'split_required';
|