clementine-agent 1.18.75 → 1.18.77
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/advisor-rules/engine.js +7 -3
- package/dist/agent/execution-advisor.js +1 -1
- package/dist/channels/discord-agent-bot.js +1 -1
- package/dist/cli/cron.js +1 -1
- package/dist/cli/dashboard.js +196 -17
- package/dist/gateway/cron-scheduler.js +35 -2
- package/dist/gateway/job-health.js +10 -2
- package/dist/types.d.ts +26 -2
- package/package.json +1 -1
|
@@ -70,14 +70,18 @@ export function evaluateWhen(c, ctx) {
|
|
|
70
70
|
const lastRun = ctx.recentRuns[0];
|
|
71
71
|
if (!lastRun)
|
|
72
72
|
return false;
|
|
73
|
-
|
|
73
|
+
// finishedAt is optional (in-progress runs are 'running' with no
|
|
74
|
+
// finishedAt). Fall back to startedAt so the rule still has a clock.
|
|
75
|
+
const lastRunTime = new Date(lastRun.finishedAt ?? lastRun.startedAt).getTime();
|
|
74
76
|
return ctx.nowMs - lastRunTime > c.ms;
|
|
75
77
|
}
|
|
76
78
|
case 'lastRunWithinMs': {
|
|
77
79
|
const lastRun = ctx.recentRuns[0];
|
|
78
80
|
if (!lastRun)
|
|
79
81
|
return false;
|
|
80
|
-
|
|
82
|
+
// finishedAt is optional (in-progress runs are 'running' with no
|
|
83
|
+
// finishedAt). Fall back to startedAt so the rule still has a clock.
|
|
84
|
+
const lastRunTime = new Date(lastRun.finishedAt ?? lastRun.startedAt).getTime();
|
|
81
85
|
return ctx.nowMs - lastRunTime <= c.ms;
|
|
82
86
|
}
|
|
83
87
|
case 'noRecentRuns':
|
|
@@ -185,7 +189,7 @@ const TEMPLATE_VARS = {
|
|
|
185
189
|
const lastRun = ctx.recentRuns[0];
|
|
186
190
|
if (!lastRun)
|
|
187
191
|
return 0;
|
|
188
|
-
const lastRunTime = new Date(lastRun.finishedAt).getTime();
|
|
192
|
+
const lastRunTime = new Date(lastRun.finishedAt ?? lastRun.startedAt).getTime();
|
|
189
193
|
const elapsed = ctx.nowMs - lastRunTime;
|
|
190
194
|
const cooldown = 60 * 60 * 1000;
|
|
191
195
|
return Math.max(0, Math.ceil((cooldown - elapsed) / 60_000));
|
|
@@ -75,7 +75,7 @@ function computeLegacyAdvice(jobName, job) {
|
|
|
75
75
|
advice.skipReason = `${consecutiveErrors} consecutive errors — circuit breaker engaged`;
|
|
76
76
|
return advice;
|
|
77
77
|
}
|
|
78
|
-
const lastRunTime = new Date(lastRun.finishedAt).getTime();
|
|
78
|
+
const lastRunTime = new Date(lastRun.finishedAt ?? lastRun.startedAt).getTime();
|
|
79
79
|
const elapsed = Date.now() - lastRunTime;
|
|
80
80
|
if (elapsed < CIRCUIT_BREAKER_COOLDOWN_MS) {
|
|
81
81
|
advice.shouldSkip = true;
|
|
@@ -366,7 +366,7 @@ export class AgentBotClient {
|
|
|
366
366
|
if (recent.length > 0) {
|
|
367
367
|
const r = recent[0];
|
|
368
368
|
const icon = r.status === 'ok' ? '\u2705' : r.status === 'error' ? '\u274C' : '\u23ED';
|
|
369
|
-
const ago = Math.round((Date.now() - new Date(r.finishedAt).getTime()) / 60000);
|
|
369
|
+
const ago = Math.round((Date.now() - new Date(r.finishedAt ?? r.startedAt).getTime()) / 60000);
|
|
370
370
|
lastRunLines.push(`${icon} ${display} \u2014 ${formatAgentDuration(ago)} ago`);
|
|
371
371
|
}
|
|
372
372
|
else {
|
package/dist/cli/cron.js
CHANGED
|
@@ -48,7 +48,7 @@ export async function cmdCronList() {
|
|
|
48
48
|
const status = job.enabled ? 'enabled' : 'disabled';
|
|
49
49
|
const recent = runLog.readRecent(job.name, 1);
|
|
50
50
|
const lastRun = recent.length > 0
|
|
51
|
-
? `last run: ${recent[0].finishedAt.slice(0, 16).replace('T', ' ')} (${recent[0].status})`
|
|
51
|
+
? `last run: ${(recent[0].finishedAt ?? recent[0].startedAt).slice(0, 16).replace('T', ' ')} (${recent[0].status})`
|
|
52
52
|
: 'never run';
|
|
53
53
|
const errors = runLog.consecutiveErrors(job.name);
|
|
54
54
|
const errorTag = errors > 0 ? ` [${errors} consecutive error(s)]` : '';
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -1350,20 +1350,13 @@ function getCronJobs() {
|
|
|
1350
1350
|
}
|
|
1351
1351
|
catch { /* ignore */ }
|
|
1352
1352
|
}
|
|
1353
|
-
// Attach recent run history
|
|
1354
|
-
|
|
1353
|
+
// Attach recent run history. Single source of truth via CronRunLog.readRecent
|
|
1354
|
+
// — same path the new /api/cron/runs cross-job endpoint uses, so per-card
|
|
1355
|
+
// last-run and the Recent History zone never disagree.
|
|
1356
|
+
const log = new CronRunLog();
|
|
1355
1357
|
const enriched = jobs.map((job) => {
|
|
1356
1358
|
const name = String(job.name ?? '');
|
|
1357
|
-
const
|
|
1358
|
-
const logPath = path.join(runsDir, `${safe}.jsonl`);
|
|
1359
|
-
let recentRuns = [];
|
|
1360
|
-
if (existsSync(logPath)) {
|
|
1361
|
-
try {
|
|
1362
|
-
const lines = readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean);
|
|
1363
|
-
recentRuns = lines.slice(-10).map((l) => JSON.parse(l)).reverse();
|
|
1364
|
-
}
|
|
1365
|
-
catch { /* ignore */ }
|
|
1366
|
-
}
|
|
1359
|
+
const recentRuns = log.readRecent(name, 10);
|
|
1367
1360
|
return { ...job, recentRuns };
|
|
1368
1361
|
});
|
|
1369
1362
|
return { jobs: enriched };
|
|
@@ -4597,6 +4590,25 @@ export async function cmdDashboard(opts) {
|
|
|
4597
4590
|
app.post('/api/cron/run/:job', (req, res) => {
|
|
4598
4591
|
const jobName = req.params.job;
|
|
4599
4592
|
try {
|
|
4593
|
+
// Concurrency lock (1.18.76+): the CronScheduler persists a sidecar
|
|
4594
|
+
// ~/.clementine/cron-running.json with one entry per in-flight job.
|
|
4595
|
+
// If a manual Run Now collides with an already-running job (whether
|
|
4596
|
+
// from another Run Now click or a scheduled tick), reject 409 so the
|
|
4597
|
+
// user gets immediate feedback instead of two parallel runs racing
|
|
4598
|
+
// each other into the same outputs.
|
|
4599
|
+
try {
|
|
4600
|
+
const runningFile = path.join(BASE_DIR, 'cron-running.json');
|
|
4601
|
+
if (existsSync(runningFile)) {
|
|
4602
|
+
const entries = JSON.parse(readFileSync(runningFile, 'utf-8'));
|
|
4603
|
+
const collision = Array.isArray(entries) && entries.find(e => String(e.jobName ?? '').toLowerCase() === jobName.toLowerCase());
|
|
4604
|
+
if (collision) {
|
|
4605
|
+
const since = collision.startedAt ? ` (running since ${collision.startedAt})` : '';
|
|
4606
|
+
res.status(409).json({ ok: false, error: `Job "${jobName}" is already running${since}` });
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
catch { /* if the sidecar is unreadable, fall through and let the runner handle dedup */ }
|
|
4600
4612
|
const child = spawn('node', [DIST_ENTRY, 'cron', 'run', jobName], {
|
|
4601
4613
|
detached: true,
|
|
4602
4614
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -6356,7 +6368,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6356
6368
|
// ── CRON CRUD routes (continued) ──────────────────────────────
|
|
6357
6369
|
app.post('/api/cron', (req, res) => {
|
|
6358
6370
|
try {
|
|
6359
|
-
const { name, schedule, prompt, tier, enabled, work_dir, mode, max_hours, max_retries, after, agent, context, skills, allowedTools, allowedMcpServers, tags, category, predictable,
|
|
6371
|
+
const { name, schedule, prompt, tier, enabled, work_dir, mode, max_hours, max_retries, after, agent, context, skills, allowedTools, allowedMcpServers, tags, category, predictable,
|
|
6372
|
+
// PRD Phase 1 fields (camelCase from API; written as snake_case YAML).
|
|
6373
|
+
successCriteriaText, successSchema, addDirs, } = req.body;
|
|
6360
6374
|
if (!name || !schedule || !prompt) {
|
|
6361
6375
|
res.status(400).json({ error: 'name, schedule, and prompt are required' });
|
|
6362
6376
|
return;
|
|
@@ -6415,6 +6429,16 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6415
6429
|
// Predictable mode — default to true (contract execution) for new
|
|
6416
6430
|
// tricks created via the dashboard. Mirror the MCP tool default.
|
|
6417
6431
|
job.predictable = (predictable === false) ? false : true;
|
|
6432
|
+
// PRD Phase 1: goal-orientation fields (camelCase from API → snake_case YAML).
|
|
6433
|
+
if (typeof successCriteriaText === 'string' && successCriteriaText.trim()) {
|
|
6434
|
+
job.success_criteria_text = successCriteriaText.trim();
|
|
6435
|
+
}
|
|
6436
|
+
if (successSchema && typeof successSchema === 'object' && !Array.isArray(successSchema) && Object.keys(successSchema).length > 0) {
|
|
6437
|
+
job.success_schema = successSchema;
|
|
6438
|
+
}
|
|
6439
|
+
if (Array.isArray(addDirs) && addDirs.length) {
|
|
6440
|
+
job.add_dirs = addDirs.map(String).map((s) => s.trim()).filter(Boolean);
|
|
6441
|
+
}
|
|
6418
6442
|
jobs.push(job);
|
|
6419
6443
|
writeCronFileAt(cronFile, parsed, jobs);
|
|
6420
6444
|
res.json({ ok: true, message: `Created cron job: ${name}` });
|
|
@@ -6556,6 +6580,36 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6556
6580
|
if (updates.predictable !== undefined) {
|
|
6557
6581
|
jobs[idx].predictable = Boolean(updates.predictable);
|
|
6558
6582
|
}
|
|
6583
|
+
// PRD Phase 1 goal fields. set-when-non-empty / delete-when-cleared,
|
|
6584
|
+
// matching the existing trick-capability pattern above. The deprecated
|
|
6585
|
+
// success_criteria array is dropped on the first save through this path.
|
|
6586
|
+
if (updates.successCriteriaText !== undefined) {
|
|
6587
|
+
const v = typeof updates.successCriteriaText === 'string' ? updates.successCriteriaText.trim() : '';
|
|
6588
|
+
if (v) {
|
|
6589
|
+
jobs[idx].success_criteria_text = v;
|
|
6590
|
+
delete jobs[idx].success_criteria; // sunset the deprecated alias on first save
|
|
6591
|
+
}
|
|
6592
|
+
else {
|
|
6593
|
+
delete jobs[idx].success_criteria_text;
|
|
6594
|
+
}
|
|
6595
|
+
}
|
|
6596
|
+
if (updates.successSchema !== undefined) {
|
|
6597
|
+
const s = updates.successSchema;
|
|
6598
|
+
if (s && typeof s === 'object' && !Array.isArray(s) && Object.keys(s).length > 0) {
|
|
6599
|
+
jobs[idx].success_schema = s;
|
|
6600
|
+
}
|
|
6601
|
+
else {
|
|
6602
|
+
delete jobs[idx].success_schema;
|
|
6603
|
+
}
|
|
6604
|
+
}
|
|
6605
|
+
if (updates.addDirs !== undefined) {
|
|
6606
|
+
if (Array.isArray(updates.addDirs) && updates.addDirs.length) {
|
|
6607
|
+
jobs[idx].add_dirs = updates.addDirs.map(String).map((s) => s.trim()).filter(Boolean);
|
|
6608
|
+
}
|
|
6609
|
+
else {
|
|
6610
|
+
delete jobs[idx].add_dirs;
|
|
6611
|
+
}
|
|
6612
|
+
}
|
|
6559
6613
|
if (updates.name !== undefined && updates.name !== bareJobName) {
|
|
6560
6614
|
// Rename — check for duplicates
|
|
6561
6615
|
const dup = jobs.find((j, i) => i !== idx && String(j.name ?? '').toLowerCase() === String(updates.name).toLowerCase());
|
|
@@ -6584,8 +6638,18 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6584
6638
|
}
|
|
6585
6639
|
jobs[idx].enabled = !jobs[idx].enabled;
|
|
6586
6640
|
writeCronFileAt(cronFile, parsed, jobs);
|
|
6587
|
-
const
|
|
6588
|
-
|
|
6641
|
+
const enabled = jobs[idx].enabled;
|
|
6642
|
+
const state = enabled ? 'enabled' : 'disabled';
|
|
6643
|
+
// Broadcast so other open dashboard tabs flip the toggle without polling.
|
|
6644
|
+
try {
|
|
6645
|
+
broadcastEvent({ type: 'cron_toggled', data: { job: bareJobName, enabled } });
|
|
6646
|
+
}
|
|
6647
|
+
catch { /* non-fatal */ }
|
|
6648
|
+
// The CronScheduler's watchFile polls CRON.md every 2s and reloads
|
|
6649
|
+
// automatically (cron-scheduler.ts:756 watchCronFile), so the toggle
|
|
6650
|
+
// takes effect within 2 seconds without an explicit reload here. We
|
|
6651
|
+
// surface a hint to the user about that latency in the response.
|
|
6652
|
+
res.json({ ok: true, message: `${jobName} is now ${state}`, enabled });
|
|
6589
6653
|
}
|
|
6590
6654
|
catch (err) {
|
|
6591
6655
|
res.status(500).json({ error: String(err) });
|
|
@@ -19976,6 +20040,32 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
19976
20040
|
</div>
|
|
19977
20041
|
</div>
|
|
19978
20042
|
|
|
20043
|
+
<!-- ── PRD Phase 1: Goal (success_criteria_text + success_schema) ──
|
|
20044
|
+
The single most important new field set. Without one of these, a
|
|
20045
|
+
run "finished"; with one, a run "accomplished what it was meant
|
|
20046
|
+
to". Banner warns (does not block) when neither is set. -->
|
|
20047
|
+
<div class="cron-section-card">
|
|
20048
|
+
<h4>Goal <span style="color:var(--text-muted);font-weight:normal;font-size:12px">— how do you know this task succeeded?</span></h4>
|
|
20049
|
+
<p class="cron-section-desc">Optional but strongly recommended. Use plain English (an evaluator agent grades the run) or a JSON Schema (validated against the agent's structured output).</p>
|
|
20050
|
+
<div id="cron-goal-warning" style="display:none;margin-bottom:12px;padding:10px 12px;border-radius:6px;background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.30);color:var(--yellow);font-size:12px">
|
|
20051
|
+
⚠ No goal set — runs will be marked "finished" but not "accomplished". Add a success criterion below or a JSON Schema.
|
|
20052
|
+
</div>
|
|
20053
|
+
<div class="form-group">
|
|
20054
|
+
<label class="form-label">Success criterion <span style="color:var(--text-muted);font-weight:normal">(plain English)</span></label>
|
|
20055
|
+
<textarea id="cron-success-criteria-text" rows="3" placeholder="e.g. 'A daily briefing email was sent to nathan@example.com containing the top 3 overnight items.'" oninput="updateGoalWarning()"></textarea>
|
|
20056
|
+
<div class="form-hint">An evaluator sub-agent reads the run's output and this criterion, then emits pass/fail with reasoning.</div>
|
|
20057
|
+
</div>
|
|
20058
|
+
<div class="form-group" style="margin-bottom:0">
|
|
20059
|
+
<details>
|
|
20060
|
+
<summary style="cursor:pointer;font-size:12px;color:var(--text-secondary);font-weight:500;padding:6px 0">▾ Success schema (JSON Schema, advanced)</summary>
|
|
20061
|
+
<div style="margin-top:8px">
|
|
20062
|
+
<textarea id="cron-success-schema" rows="6" placeholder='{ "type": "object", "required": ["sent"], "properties": { "sent": { "type": "boolean" } } }' style="font-family:'JetBrains Mono',monospace;font-size:11px" oninput="updateGoalWarning()"></textarea>
|
|
20063
|
+
<div class="form-hint">JSON Schema validated against the agent's <code>structured_output</code>. Mechanically successful = parses + validates.</div>
|
|
20064
|
+
</div>
|
|
20065
|
+
</details>
|
|
20066
|
+
</div>
|
|
20067
|
+
</div>
|
|
20068
|
+
|
|
19979
20069
|
<!-- Skills & tools: pinned skills + MCP + tools + tags -->
|
|
19980
20070
|
<div class="cron-section-card">
|
|
19981
20071
|
<h4>Skills & tools</h4>
|
|
@@ -20052,6 +20142,14 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
20052
20142
|
<div class="form-hint">Run inside a project directory. Agent gets that project's CLAUDE.md.</div>
|
|
20053
20143
|
</div>
|
|
20054
20144
|
</div>
|
|
20145
|
+
<!-- PRD Phase 1: read scope beyond cwd. One absolute path per line. -->
|
|
20146
|
+
<div class="form-row">
|
|
20147
|
+
<div class="form-group" style="flex:1">
|
|
20148
|
+
<label class="form-label">Additional read directories <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
|
|
20149
|
+
<textarea id="cron-add-dirs" rows="2" placeholder="/Users/me/notes /Users/me/clients/acme" style="font-family:'JetBrains Mono',monospace;font-size:11px"></textarea>
|
|
20150
|
+
<div class="form-hint">One absolute path per line. The agent gets read access to these in addition to the Project Context cwd.</div>
|
|
20151
|
+
</div>
|
|
20152
|
+
</div>
|
|
20055
20153
|
<div class="form-row">
|
|
20056
20154
|
<div class="form-group">
|
|
20057
20155
|
<label class="form-label">Mode</label>
|
|
@@ -23441,7 +23539,7 @@ async function openTraceViewer(jobName) {
|
|
|
23441
23539
|
traceData = d.traces || [];
|
|
23442
23540
|
|
|
23443
23541
|
if (traceData.length === 0) {
|
|
23444
|
-
document.getElementById('trace-content').innerHTML = '<div style="padding:
|
|
23542
|
+
document.getElementById('trace-content').innerHTML = '<div style="padding:24px;color:var(--text-muted);line-height:1.6"><div style="font-weight:500;color:var(--text-secondary);margin-bottom:8px">No traces recorded yet</div><div style="font-size:12px">Traces are captured per run and show every tool call the agent made.<br/>If a recent run errored before any tool was invoked, see the <strong>Recent history</strong> zone on the Tasks page for the error message.</div></div>';
|
|
23445
23543
|
document.getElementById('trace-run-selector').innerHTML = '';
|
|
23446
23544
|
return;
|
|
23447
23545
|
}
|
|
@@ -23493,7 +23591,7 @@ function renderTrace(idx) {
|
|
|
23493
23591
|
+ '</div>';
|
|
23494
23592
|
}
|
|
23495
23593
|
|
|
23496
|
-
document.getElementById('trace-content').innerHTML = html || '<div style="padding:
|
|
23594
|
+
document.getElementById('trace-content').innerHTML = html || '<div style="padding:24px;color:var(--text-muted);line-height:1.6"><div style="font-weight:500;color:var(--text-secondary);margin-bottom:8px">This run produced no tool calls</div><div style="font-size:12px">The run started but didn\\x27t reach any tool — it likely errored before the agent acted, or was a no-op response.<br/>Check <strong>Recent history</strong> on the Tasks page for the error message or output preview from this run.</div></div>';
|
|
23497
23595
|
}
|
|
23498
23596
|
|
|
23499
23597
|
// ── Inline Trace (Execution tab) ──
|
|
@@ -24697,6 +24795,18 @@ function renderCronLegacyBanner(job) {
|
|
|
24697
24795
|
+ '</div>';
|
|
24698
24796
|
}
|
|
24699
24797
|
|
|
24798
|
+
// PRD Phase 1: show a non-blocking warning under the Goal section header
|
|
24799
|
+
// when neither success_criteria_text nor success_schema is set. The PRD's
|
|
24800
|
+
// "the run accomplished what it was supposed to" promise depends on at
|
|
24801
|
+
// least one of the two being present.
|
|
24802
|
+
function updateGoalWarning() {
|
|
24803
|
+
var sct = (document.getElementById('cron-success-criteria-text')?.value || '').trim();
|
|
24804
|
+
var ssc = (document.getElementById('cron-success-schema')?.value || '').trim();
|
|
24805
|
+
var warn = document.getElementById('cron-goal-warning');
|
|
24806
|
+
if (!warn) return;
|
|
24807
|
+
warn.style.display = (!sct && !ssc) ? '' : 'none';
|
|
24808
|
+
}
|
|
24809
|
+
|
|
24700
24810
|
// One-click migration: flip predictable=true AND save immediately so the
|
|
24701
24811
|
// user doesn't have to remember to also click Save Changes.
|
|
24702
24812
|
async function enablePredictableFromBanner() {
|
|
@@ -24738,6 +24848,11 @@ function openCreateCronModal(agentSlug) {
|
|
|
24738
24848
|
toggleUnleashedOptions();
|
|
24739
24849
|
document.getElementById('cron-prompt').value = '';
|
|
24740
24850
|
document.getElementById('cron-context').value = '';
|
|
24851
|
+
// PRD Phase 1 goal fields — empty by default. Warning banner will show.
|
|
24852
|
+
var sct = document.getElementById('cron-success-criteria-text'); if (sct) sct.value = '';
|
|
24853
|
+
var ssc = document.getElementById('cron-success-schema'); if (ssc) ssc.value = '';
|
|
24854
|
+
var addDirsEl = document.getElementById('cron-add-dirs'); if (addDirsEl) addDirsEl.value = '';
|
|
24855
|
+
if (typeof updateGoalWarning === 'function') updateGoalWarning();
|
|
24741
24856
|
document.getElementById('cron-training-section').style.display = 'none';
|
|
24742
24857
|
document.getElementById('cron-train-btn').style.display = '';
|
|
24743
24858
|
resetCronTrainingChat();
|
|
@@ -24784,6 +24899,30 @@ function openEditCronModal(jobName) {
|
|
|
24784
24899
|
toggleUnleashedOptions();
|
|
24785
24900
|
document.getElementById('cron-prompt').value = job.prompt || '';
|
|
24786
24901
|
document.getElementById('cron-context').value = job.context || '';
|
|
24902
|
+
// PRD Phase 1: load goal fields. Accept either casing — old YAML may have
|
|
24903
|
+
// success_criteria as a list (legacy); the parser already coalesces those
|
|
24904
|
+
// into successCriteriaText on read, but defend here too in case the API
|
|
24905
|
+
// shape differs from what the parser produces.
|
|
24906
|
+
var sctE = document.getElementById('cron-success-criteria-text');
|
|
24907
|
+
if (sctE) {
|
|
24908
|
+
var sctVal = job.successCriteriaText || job.success_criteria_text || '';
|
|
24909
|
+
if (!sctVal && Array.isArray(job.successCriteria || job.success_criteria)) {
|
|
24910
|
+
sctVal = (job.successCriteria || job.success_criteria || []).join('\\n');
|
|
24911
|
+
}
|
|
24912
|
+
sctE.value = sctVal;
|
|
24913
|
+
}
|
|
24914
|
+
var sscE = document.getElementById('cron-success-schema');
|
|
24915
|
+
if (sscE) {
|
|
24916
|
+
var sscObj = job.successSchema || job.success_schema;
|
|
24917
|
+
sscE.value = (sscObj && typeof sscObj === 'object') ? JSON.stringify(sscObj, null, 2) : '';
|
|
24918
|
+
}
|
|
24919
|
+
var addDirsE = document.getElementById('cron-add-dirs');
|
|
24920
|
+
if (addDirsE) {
|
|
24921
|
+
var addDirsArr = Array.isArray(job.addDirs) ? job.addDirs
|
|
24922
|
+
: (Array.isArray(job.add_dirs) ? job.add_dirs : []);
|
|
24923
|
+
addDirsE.value = addDirsArr.join('\\n');
|
|
24924
|
+
}
|
|
24925
|
+
if (typeof updateGoalWarning === 'function') updateGoalWarning();
|
|
24787
24926
|
document.getElementById('cron-training-section').style.display = 'none';
|
|
24788
24927
|
document.getElementById('cron-train-btn').style.display = '';
|
|
24789
24928
|
resetCronTrainingChat();
|
|
@@ -24979,6 +25118,11 @@ function captureCronModalSnapshot() {
|
|
|
24979
25118
|
v('cron-workdir'),
|
|
24980
25119
|
v('cron-allowed-tools'),
|
|
24981
25120
|
v('cron-category'),
|
|
25121
|
+
// PRD Phase 1 goal fields — included in dirty check so leaving the
|
|
25122
|
+
// modal with an unsaved success_schema or success_criteria_text prompts.
|
|
25123
|
+
v('cron-success-criteria-text'),
|
|
25124
|
+
v('cron-success-schema'),
|
|
25125
|
+
v('cron-add-dirs'),
|
|
24982
25126
|
(document.getElementById('cron-predictable') || {}).checked ? '1' : '0',
|
|
24983
25127
|
JSON.stringify(_cronSelectedSkills || []),
|
|
24984
25128
|
JSON.stringify(_cronSelectedMcp || []),
|
|
@@ -25003,6 +25147,9 @@ function isCronModalDirty() {
|
|
|
25003
25147
|
v('cron-workdir'),
|
|
25004
25148
|
v('cron-allowed-tools'),
|
|
25005
25149
|
v('cron-category'),
|
|
25150
|
+
v('cron-success-criteria-text'),
|
|
25151
|
+
v('cron-success-schema'),
|
|
25152
|
+
v('cron-add-dirs'),
|
|
25006
25153
|
(document.getElementById('cron-predictable') || {}).checked ? '1' : '0',
|
|
25007
25154
|
JSON.stringify(_cronSelectedSkills || []),
|
|
25008
25155
|
JSON.stringify(_cronSelectedMcp || []),
|
|
@@ -25177,6 +25324,34 @@ async function saveCronJob() {
|
|
|
25177
25324
|
}
|
|
25178
25325
|
if (!prompt) { toast('Prompt is required — tell the agent what to do', 'error'); document.getElementById('cron-prompt').focus(); return; }
|
|
25179
25326
|
|
|
25327
|
+
// PRD Phase 1 goal fields. successCriteriaText is freeform; successSchema
|
|
25328
|
+
// is parsed JSON. Validate JSON early so the user gets a clean error before
|
|
25329
|
+
// the round-trip. Empty schema is fine — we just send {} or undefined.
|
|
25330
|
+
var successCriteriaText = (document.getElementById('cron-success-criteria-text')?.value || '').trim();
|
|
25331
|
+
var successSchemaRaw = (document.getElementById('cron-success-schema')?.value || '').trim();
|
|
25332
|
+
var successSchema;
|
|
25333
|
+
if (successSchemaRaw) {
|
|
25334
|
+
try {
|
|
25335
|
+
successSchema = JSON.parse(successSchemaRaw);
|
|
25336
|
+
if (!successSchema || typeof successSchema !== 'object' || Array.isArray(successSchema)) {
|
|
25337
|
+
toast('Success schema must be a JSON object', 'error');
|
|
25338
|
+
document.getElementById('cron-success-schema').focus();
|
|
25339
|
+
return;
|
|
25340
|
+
}
|
|
25341
|
+
} catch (e) {
|
|
25342
|
+
toast('Success schema is not valid JSON: ' + (e.message || String(e)), 'error');
|
|
25343
|
+
document.getElementById('cron-success-schema').focus();
|
|
25344
|
+
return;
|
|
25345
|
+
}
|
|
25346
|
+
}
|
|
25347
|
+
// add_dirs: one absolute path per line. Trim, dedupe, drop blanks.
|
|
25348
|
+
var addDirsRaw = (document.getElementById('cron-add-dirs')?.value || '').split(/\\r?\\n/);
|
|
25349
|
+
var addDirs = addDirsRaw.map(function(s){ return s.trim(); }).filter(Boolean);
|
|
25350
|
+
// Quick sanity — warn but don't block on relative paths.
|
|
25351
|
+
if (addDirs.some(function(p){ return !p.startsWith('/') && !p.startsWith('~'); })) {
|
|
25352
|
+
toast('Heads up: add_dirs entries should be absolute paths.', 'info');
|
|
25353
|
+
}
|
|
25354
|
+
|
|
25180
25355
|
const body = {
|
|
25181
25356
|
name, schedule, tier, prompt, enabled: true,
|
|
25182
25357
|
work_dir: work_dir || undefined, mode, max_hours, max_retries, after, context,
|
|
@@ -25194,6 +25369,10 @@ async function saveCronJob() {
|
|
|
25194
25369
|
tags: editingCronJob ? _cronTags : (_cronTags.length ? _cronTags : undefined),
|
|
25195
25370
|
category: editingCronJob ? (category || '') : category,
|
|
25196
25371
|
predictable,
|
|
25372
|
+
// PRD Phase 1 goal-orientation. PUT delete-on-empty pattern below.
|
|
25373
|
+
successCriteriaText: editingCronJob ? successCriteriaText : (successCriteriaText || undefined),
|
|
25374
|
+
successSchema: editingCronJob ? (successSchema || null) : (successSchema || undefined),
|
|
25375
|
+
addDirs: editingCronJob ? addDirs : (addDirs.length ? addDirs : undefined),
|
|
25197
25376
|
};
|
|
25198
25377
|
|
|
25199
25378
|
var wasEditing = !!editingCronJob;
|
|
@@ -113,6 +113,24 @@ export function parseCronJobs() {
|
|
|
113
113
|
const successCriteria = Array.isArray(job.success_criteria)
|
|
114
114
|
? job.success_criteria.map(c => String(c))
|
|
115
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);
|
|
116
134
|
const alwaysDeliver = job.always_deliver === true ? true : undefined;
|
|
117
135
|
const context = job.context != null ? String(job.context) : undefined;
|
|
118
136
|
const preCheck = job.pre_check != null ? String(job.pre_check) : undefined;
|
|
@@ -140,7 +158,8 @@ export function parseCronJobs() {
|
|
|
140
158
|
}
|
|
141
159
|
jobs.push({
|
|
142
160
|
name, schedule, prompt, enabled, tier, maxTurns, model, workDir, mode,
|
|
143
|
-
maxHours, maxRetries, after, successCriteria,
|
|
161
|
+
maxHours, maxRetries, after, successCriteria, successCriteriaText, successSchema, addDirs,
|
|
162
|
+
alwaysDeliver, context, preCheck, agentSlug,
|
|
144
163
|
skills, allowedTools, allowedMcpServers, tags, category, predictable,
|
|
145
164
|
});
|
|
146
165
|
}
|
|
@@ -187,6 +206,18 @@ export function parseAgentCronJobs(agentsDir) {
|
|
|
187
206
|
const successCriteria = Array.isArray(job.success_criteria)
|
|
188
207
|
? job.success_criteria.map(c => String(c))
|
|
189
208
|
: undefined;
|
|
209
|
+
// PRD Phase 1 fields — symmetric with global parser above.
|
|
210
|
+
let successCriteriaText = typeof job.success_criteria_text === 'string'
|
|
211
|
+
? String(job.success_criteria_text)
|
|
212
|
+
: (typeof job.successCriteriaText === 'string' ? String(job.successCriteriaText) : undefined);
|
|
213
|
+
if (!successCriteriaText && Array.isArray(successCriteria) && successCriteria.length > 0) {
|
|
214
|
+
successCriteriaText = successCriteria.join('\n');
|
|
215
|
+
}
|
|
216
|
+
const successSchemaRaw = job.success_schema ?? job.successSchema;
|
|
217
|
+
const successSchema = (successSchemaRaw && typeof successSchemaRaw === 'object' && !Array.isArray(successSchemaRaw))
|
|
218
|
+
? successSchemaRaw
|
|
219
|
+
: undefined;
|
|
220
|
+
const addDirs = normalizeStringArray(job.add_dirs ?? job.addDirs);
|
|
190
221
|
const context = job.context != null ? String(job.context) : undefined;
|
|
191
222
|
const preCheck = job.pre_check != null ? String(job.pre_check) : undefined;
|
|
192
223
|
// ── Trick capabilities — symmetric with global parser ─────────
|
|
@@ -210,7 +241,9 @@ export function parseAgentCronJobs(agentsDir) {
|
|
|
210
241
|
allJobs.push({
|
|
211
242
|
name: `${slug}:${name}`,
|
|
212
243
|
schedule, prompt, enabled, tier, maxTurns, model, workDir,
|
|
213
|
-
mode, maxHours, maxRetries, after,
|
|
244
|
+
mode, maxHours, maxRetries, after,
|
|
245
|
+
successCriteria, successCriteriaText, successSchema, addDirs,
|
|
246
|
+
context, preCheck,
|
|
214
247
|
agentSlug: slug,
|
|
215
248
|
skills, allowedTools, allowedMcpServers, tags, category, predictable,
|
|
216
249
|
});
|
|
@@ -85,12 +85,20 @@ export function classifyRunHealth(entry) {
|
|
|
85
85
|
requiresApproval: true,
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
// 'timeout' and 'lost' are explicit terminal failure states (1.18.76+).
|
|
89
|
+
// 'timeout' = the job exceeded max_hours. 'lost' = the daemon-boot sweep
|
|
90
|
+
// closed an orphaned 'running' entry that never got a finishedAt because
|
|
91
|
+
// the daemon crashed mid-run.
|
|
92
|
+
if (entry.status === 'error' || entry.status === 'retried' || entry.status === 'timeout' || entry.status === 'lost') {
|
|
89
93
|
return {
|
|
90
94
|
...base,
|
|
91
95
|
status: 'failed',
|
|
92
96
|
evidence: compactEvidence(entry.error, entry.outputPreview, terminalReason ? `terminalReason=${terminalReason}` : undefined),
|
|
93
|
-
recommendedAction:
|
|
97
|
+
recommendedAction: entry.status === 'lost'
|
|
98
|
+
? 'The daemon crashed before this run completed. Check the daemon logs around the startedAt timestamp; if this repeats, lower max_hours or investigate the underlying job.'
|
|
99
|
+
: entry.status === 'timeout'
|
|
100
|
+
? 'The job exceeded its time budget. Lower max_hours, narrow the prompt, or split into smaller steps.'
|
|
101
|
+
: 'Inspect the latest error and current job definition before applying a fix.',
|
|
94
102
|
requiresApproval: true,
|
|
95
103
|
};
|
|
96
104
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -328,7 +328,22 @@ export interface CronJobDefinition {
|
|
|
328
328
|
maxRetries?: number;
|
|
329
329
|
after?: string;
|
|
330
330
|
agentSlug?: string;
|
|
331
|
+
/** @deprecated Use successCriteriaText (free-text) or successSchema (JSON Schema)
|
|
332
|
+
* per PRD Phase 1. successCriteria is kept readable for one release; on read,
|
|
333
|
+
* parseCronJobs coalesces it into successCriteriaText. */
|
|
331
334
|
successCriteria?: string[];
|
|
335
|
+
/** PRD Phase 1: free-text "this task is done when…". An evaluator sub-agent reads
|
|
336
|
+
* the run's final state and the criterion and emits a pass/fail with reasoning.
|
|
337
|
+
* Stored as RunEvaluation on the Run. Optional but recommended. */
|
|
338
|
+
successCriteriaText?: string;
|
|
339
|
+
/** PRD Phase 1: JSON Schema validated against ResultMessage.structured_output.
|
|
340
|
+
* If it parses, the run is mechanically successful. The Task editor shows a
|
|
341
|
+
* non-blocking "Goal not set" warning when neither this nor successCriteriaText
|
|
342
|
+
* is present. */
|
|
343
|
+
successSchema?: Record<string, unknown>;
|
|
344
|
+
/** PRD Phase 1: read scope beyond the cwd (workDir). Surfaced as a chip list
|
|
345
|
+
* in the editor's Scope tab. The runner passes these to the SDK as add_dirs. */
|
|
346
|
+
addDirs?: string[];
|
|
332
347
|
alwaysDeliver?: boolean;
|
|
333
348
|
context?: string;
|
|
334
349
|
preCheck?: string;
|
|
@@ -392,8 +407,17 @@ export type TerminalReason = 'blocking_limit' | 'rapid_refill_breaker' | 'prompt
|
|
|
392
407
|
export interface CronRunEntry {
|
|
393
408
|
jobName: string;
|
|
394
409
|
startedAt: string;
|
|
395
|
-
|
|
396
|
-
|
|
410
|
+
/** Optional: in-progress runs are appended with status='running' before the
|
|
411
|
+
* finishedAt is known. The runner replaces or supersedes the entry on
|
|
412
|
+
* completion. The stale-running sweep emits a closing 'lost' entry for
|
|
413
|
+
* any 'running' row whose startedAt has aged past the deadline. */
|
|
414
|
+
finishedAt?: string;
|
|
415
|
+
/** 'ok' | 'error' | 'retried' | 'skipped' are terminal. 'running' is in-flight.
|
|
416
|
+
* 'timeout' fires when max_hours is exceeded. 'lost' is appended by the
|
|
417
|
+
* daemon-boot stale sweep when a 'running' entry has no companion close
|
|
418
|
+
* (daemon likely crashed mid-run). */
|
|
419
|
+
status: 'ok' | 'error' | 'retried' | 'skipped' | 'running' | 'timeout' | 'lost';
|
|
420
|
+
/** 0 for in-progress 'running' rows; populated when terminal. */
|
|
397
421
|
durationMs: number;
|
|
398
422
|
error?: string;
|
|
399
423
|
errorType?: 'transient' | 'permanent';
|