clementine-agent 1.18.63 → 1.18.64

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.
@@ -54,10 +54,10 @@ const CRON_FIXER_PROMPT = [
54
54
  'You are the cron-fix specialist. You diagnose and apply fixes to broken cron jobs.',
55
55
  '',
56
56
  'Workflow:',
57
- '1. Call `list_broken_jobs` to see what is currently broken with their cached diagnoses.',
57
+ '1. If you already know the job name (parent named it, or notification context names it), call `cron_diagnose` first it returns the bounded recent-run summary, phase status, and inferred root cause in one shot. If you need a list of currently failing jobs, call `list_broken_jobs` instead.',
58
58
  '2. For each job the user/parent asked about, check the proposed fix:',
59
59
  ' - confidence=high + risk=low + autoApply=true → call `apply_broken_job_fix`.',
60
- ' - Otherwise → describe the diagnosis and ask the parent for explicit approval.',
60
+ ' - Otherwise → describe the diagnosis and ask the parent for explicit approval before any manual repair.',
61
61
  '3. After applying a fix, the verification system auto-rolls-back if the next 3 runs do not improve. You do NOT need to monitor manually.',
62
62
  '',
63
63
  'Return: a one-paragraph summary of what you applied (or what is blocking apply), per job.',
@@ -154,6 +154,7 @@ export function buildAgentMap(opts = {}) {
154
154
  prompt: CRON_FIXER_PROMPT,
155
155
  model: 'sonnet',
156
156
  tools: [
157
+ 'mcp__clementine-tools__cron_diagnose',
157
158
  'mcp__clementine-tools__list_broken_jobs',
158
159
  'mcp__clementine-tools__apply_broken_job_fix',
159
160
  'mcp__clementine-tools__cron_list',
@@ -20,7 +20,4 @@ export declare function detectCronDiagnosticRequest(text: string, opts?: {
20
20
  export declare function buildCronDiagnosticResponseForRequest(request: CronDiagnosticRequest, opts?: {
21
21
  baseDir: string;
22
22
  }): string | null;
23
- export declare function buildCronDiagnosticResponse(text: string, opts?: {
24
- baseDir: string;
25
- }): string | null;
26
23
  //# sourceMappingURL=cron-diagnostic-turn.d.ts.map
@@ -274,10 +274,4 @@ export function buildCronDiagnosticResponseForRequest(request, opts = { baseDir:
274
274
  }
275
275
  return lines.join('\n');
276
276
  }
277
- export function buildCronDiagnosticResponse(text, opts = { baseDir: process.env.CLEMENTINE_HOME || '' }) {
278
- const request = detectCronDiagnosticRequest(text, { baseDir: opts.baseDir });
279
- if (!request || !opts.baseDir)
280
- return null;
281
- return buildCronDiagnosticResponseForRequest(request, opts);
282
- }
283
277
  //# sourceMappingURL=cron-diagnostic-turn.js.map
@@ -8,7 +8,7 @@
8
8
  import path from 'node:path';
9
9
  import { listBackgroundTasks } from '../agent/background-tasks.js';
10
10
  import { buildNotificationContextPrompt, findRecentNotificationContext, looksLikeNotificationFollowup, } from './notification-context.js';
11
- import { buildCronDiagnosticResponseForRequest, detectCronDiagnosticRequest, isInternalSyntheticPrompt, } from './cron-diagnostic-turn.js';
11
+ import { detectCronDiagnosticRequest, isInternalSyntheticPrompt, } from './cron-diagnostic-turn.js';
12
12
  export { isInternalSyntheticPrompt } from './cron-diagnostic-turn.js';
13
13
  const RECENT_TASK_TTL_MS = 24 * 60 * 60 * 1000;
14
14
  function normalizeForMatch(text) {
@@ -22,48 +22,6 @@ function normalizeForMatch(text) {
22
22
  function compactWhitespace(text) {
23
23
  return text.replace(/\s+/g, ' ').trim();
24
24
  }
25
- function wantsFix(text) {
26
- return /\b(fix|repair|solve|handle|diagnose|debug|what broke|what happened|why did|why is|issue|problem|failure|failed|failing)\b/i.test(text);
27
- }
28
- function jobMentionedInText(jobName, text) {
29
- const normalizedText = normalizeForMatch(text);
30
- const normalizedJob = normalizeForMatch(jobName);
31
- return !!normalizedJob && normalizedText.includes(normalizedJob);
32
- }
33
- function resolveNotificationJob(event, userText, baseDir) {
34
- const explicit = detectCronDiagnosticRequest(userText, { baseDir });
35
- if (explicit?.jobName)
36
- return explicit.jobName;
37
- const jobs = event.jobNames ?? [];
38
- if (jobs.length === 1)
39
- return jobs[0] ?? null;
40
- const mentioned = jobs.find((job) => jobMentionedInText(job, userText));
41
- return mentioned ?? null;
42
- }
43
- function summarizeCronNotification(event, userText, opts) {
44
- const jobs = event.jobNames ?? [];
45
- if (jobs.length === 0)
46
- return null;
47
- const targetJob = resolveNotificationJob(event, userText, opts.baseDir);
48
- if (targetJob) {
49
- return buildCronDiagnosticResponseForRequest({ jobName: targetJob, wantsFix: wantsFix(userText) }, { baseDir: opts.baseDir });
50
- }
51
- const lines = [
52
- `I am resolving this to the recent cron failure alert: ${jobs.join(', ')}.`,
53
- 'More than one job was in that alert, so I am not going to guess or start background work.',
54
- '',
55
- ];
56
- for (const job of jobs.slice(0, 5)) {
57
- const diagnostic = buildCronDiagnosticResponseForRequest({ jobName: job, wantsFix: false }, { baseDir: opts.baseDir });
58
- const preview = diagnostic
59
- ? diagnostic.split('\n').slice(1, 4).join(' ')
60
- : 'No local diagnostic summary available.';
61
- lines.push(`- ${job}: ${compactWhitespace(preview).slice(0, 260)}`);
62
- }
63
- lines.push('');
64
- lines.push(`Reply \`fix ${jobs[0]}\` or name the job you want me to repair first.`);
65
- return lines.join('\n');
66
- }
67
25
  function taskMatchesSession(task, sessionKey) {
68
26
  return task.sessionKey === sessionKey;
69
27
  }
@@ -151,22 +109,11 @@ export function resolveRecentOperationalContext(sessionKey, text, opts) {
151
109
  now: opts.now,
152
110
  });
153
111
  if (notification) {
154
- if (notification.type === 'cron_failure') {
155
- const responseText = summarizeCronNotification(notification, text, opts);
156
- if (responseText) {
157
- return {
158
- source: 'notification',
159
- reason: 'vague-followup-to-cron-failure-notification',
160
- responseText,
161
- suppressDeepMode: true,
162
- eventId: notification.id,
163
- jobNames: notification.jobNames,
164
- };
165
- }
166
- }
167
112
  return {
168
113
  source: 'notification',
169
- reason: 'vague-followup-to-proactive-notification',
114
+ reason: notification.type === 'cron_failure'
115
+ ? 'vague-followup-to-cron-failure-notification'
116
+ : 'vague-followup-to-proactive-notification',
170
117
  promptText: buildNotificationContextPrompt(notification, text),
171
118
  suppressDeepMode: true,
172
119
  eventId: notification.id,
@@ -20,7 +20,6 @@ import { listBackgroundTasks, loadBackgroundTask, markFailed } from '../agent/ba
20
20
  import { applyAssistantExperienceUpdate, detectApprovalReply, detectLocalTurn } from '../agent/local-turn.js';
21
21
  import { buildApprovalFollowupPrompt, detectActionExpectation } from '../agent/action-enforcer.js';
22
22
  import { updateClementineJson } from '../config/clementine-json.js';
23
- import { buildCronDiagnosticResponse } from './cron-diagnostic-turn.js';
24
23
  import { classifyIntent } from '../agent/intent-classifier.js';
25
24
  import { decideTurn } from '../agent/turn-policy.js';
26
25
  import { recordProactiveNotificationEvent, } from './notification-context.js';
@@ -1441,34 +1440,6 @@ export class Gateway {
1441
1440
  text = recentContext.promptText;
1442
1441
  }
1443
1442
  }
1444
- // Cron "what broke / fix this job" asks should not spin up a broad SDK
1445
- // session. They are bounded local diagnostics over run summaries and scalar
1446
- // config only, and they intentionally do not execute the cron job.
1447
- if (this.isTrustedPersonalSession(sessionKey) && !isInternalSyntheticPrompt(text)) {
1448
- const cronDiagnostic = buildCronDiagnosticResponse(text, { baseDir: BASE_DIR });
1449
- if (cronDiagnostic) {
1450
- const current = this.sessions.get(sessionKey);
1451
- if (current?.abortController && !current.abortController.signal.aborted) {
1452
- current.abortController.abort('replaced-by-cron-diagnostic');
1453
- logger.info({ sessionKey }, 'Interrupted active chat for local cron diagnostic');
1454
- }
1455
- this.assistant.injectContext(sessionKey, originalText, cronDiagnostic);
1456
- if (onText) {
1457
- try {
1458
- await onText(cronDiagnostic);
1459
- }
1460
- catch { /* channel streaming is best-effort */ }
1461
- }
1462
- logger.info({
1463
- sessionKey,
1464
- totalMs: Date.now() - tInnerStart,
1465
- chatMs: Date.now() - localTurnStarted,
1466
- localCronDiagnostic: true,
1467
- responseLen: cronDiagnostic.length,
1468
- }, 'chat:latency');
1469
- return cronDiagnostic;
1470
- }
1471
- }
1472
1443
  // Show "queued" status if either lane or session lock is contended,
1473
1444
  // so the user doesn't stare at "thinking..." for up to 60s while a
1474
1445
  // previous message is still processing.
@@ -1779,5 +1779,16 @@ export function registerAdminTools(server) {
1779
1779
  `The fix-verification tracker will roll it back automatically if the next runs don't improve. ` +
1780
1780
  `Root cause: ${d.rootCause?.slice(0, 200) ?? ""}.`);
1781
1781
  });
1782
+ server.tool('cron_diagnose', 'Return a bounded deterministic diagnosis for one cron job — recent run summary, unleashed phase status, last clean success, current scalar config, and the inferred root cause. Reads only from local run history and cron config; does NOT execute the job. Use this when the user asks what is broken with a specific job, before deciding whether to call apply_broken_job_fix or to propose a manual repair.', {
1783
+ jobName: z.string().describe('The job name as shown in CRON.md or list_broken_jobs output (e.g. "audit-inbox-check" or "ross-the-sdr:reply-detection").'),
1784
+ }, async ({ jobName }) => {
1785
+ const { buildCronDiagnosticResponseForRequest } = await import('../gateway/cron-diagnostic-turn.js');
1786
+ const response = buildCronDiagnosticResponseForRequest({ jobName, wantsFix: true }, { baseDir: BASE_DIR });
1787
+ if (!response) {
1788
+ return textResult(`No diagnostic data for \`${jobName}\` — either the job is not configured in CRON.md or there is no run history yet. ` +
1789
+ `Check the job name with cron_list or list_broken_jobs.`);
1790
+ }
1791
+ return textResult(response);
1792
+ });
1782
1793
  }
1783
1794
  //# sourceMappingURL=admin-tools.js.map
@@ -27,6 +27,8 @@ mac:
27
27
  target:
28
28
  - dmg
29
29
  - zip
30
+ dmg:
31
+ sign: true
30
32
  artifactName: Clementine-${version}-mac-${arch}.${ext}
31
33
  publish:
32
34
  provider: github
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.63",
3
+ "version": "1.18.64",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,7 +18,8 @@
18
18
  "desktop:debug": "npm run build && CLEMENTINE_DESKTOP_DEBUG=1 electron dist/desktop/main.js",
19
19
  "desktop:prepare": "npm run build && npm rebuild better-sqlite3",
20
20
  "desktop:pack": "npm run desktop:prepare && electron-builder --mac --dir",
21
- "desktop:dist": "npm run desktop:prepare && electron-builder --mac",
21
+ "desktop:dist": "scripts/build-desktop-mac.sh",
22
+ "desktop:dist:unnotarized": "CLEMENTINE_ALLOW_UNNOTARIZED=1 scripts/build-desktop-mac.sh",
22
23
  "mcp": "tsx src/tools/mcp-server.ts",
23
24
  "cli": "tsx src/cli/index.ts",
24
25
  "typecheck": "tsc --noEmit",
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT_DIR"
6
+
7
+ load_env_file() {
8
+ local file="$1"
9
+ if [[ -f "$file" ]]; then
10
+ set -a
11
+ # shellcheck disable=SC1090
12
+ source "$file"
13
+ set +a
14
+ fi
15
+ }
16
+
17
+ load_env_file "$ROOT_DIR/.env.signing"
18
+ load_env_file "$ROOT_DIR/.env.release"
19
+ load_env_file "$HOME/.clementine/signing.env"
20
+
21
+ if [[ -n "${APPLE_ID_PASSWORD:-}" && -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" ]]; then
22
+ export APPLE_APP_SPECIFIC_PASSWORD="$APPLE_ID_PASSWORD"
23
+ fi
24
+
25
+ if [[ -n "${CLEMENTINE_NOTARY_PROFILE:-}" && -z "${APPLE_KEYCHAIN_PROFILE:-}" ]]; then
26
+ export APPLE_KEYCHAIN_PROFILE="$CLEMENTINE_NOTARY_PROFILE"
27
+ fi
28
+
29
+ has_password_creds=false
30
+ if [[ -n "${APPLE_ID:-}" && -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" && -n "${APPLE_TEAM_ID:-}" ]]; then
31
+ has_password_creds=true
32
+ fi
33
+
34
+ has_api_key_creds=false
35
+ if [[ -n "${APPLE_API_KEY:-}" && -n "${APPLE_API_KEY_ID:-}" && -n "${APPLE_API_ISSUER:-}" ]]; then
36
+ has_api_key_creds=true
37
+ fi
38
+
39
+ has_keychain_profile=false
40
+ if [[ -n "${APPLE_KEYCHAIN_PROFILE:-}" ]]; then
41
+ has_keychain_profile=true
42
+ fi
43
+
44
+ notarytool_args=()
45
+ if [[ "$has_password_creds" == true ]]; then
46
+ notarytool_args=(--apple-id "$APPLE_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --team-id "$APPLE_TEAM_ID")
47
+ elif [[ "$has_api_key_creds" == true ]]; then
48
+ notarytool_args=(--key "$APPLE_API_KEY" --key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER")
49
+ elif [[ "$has_keychain_profile" == true ]]; then
50
+ notarytool_args=(--keychain-profile "$APPLE_KEYCHAIN_PROFILE")
51
+ if [[ -n "${APPLE_KEYCHAIN:-}" ]]; then
52
+ notarytool_args=(--keychain "$APPLE_KEYCHAIN" "${notarytool_args[@]}")
53
+ fi
54
+ fi
55
+
56
+ if [[ "$has_password_creds" != true && "$has_api_key_creds" != true && "$has_keychain_profile" != true ]]; then
57
+ cat >&2 <<'EOF'
58
+ No Apple notarization credentials were detected, so the release DMG would be signed but not notarized.
59
+
60
+ Set one of these before running npm run desktop:dist:
61
+ - APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID
62
+ - APPLE_API_KEY, APPLE_API_KEY_ID, APPLE_API_ISSUER
63
+ - APPLE_KEYCHAIN_PROFILE
64
+
65
+ This script also accepts:
66
+ - APPLE_ID_PASSWORD as an alias for APPLE_APP_SPECIFIC_PASSWORD
67
+ - CLEMENTINE_NOTARY_PROFILE as an alias for APPLE_KEYCHAIN_PROFILE
68
+
69
+ One-time local keychain setup:
70
+ xcrun notarytool store-credentials clementine --apple-id <apple-id> --team-id 4AR3Y8XD72 --sync
71
+ CLEMENTINE_NOTARY_PROFILE=clementine npm run desktop:dist
72
+
73
+ For a local signed-but-unnotarized test build:
74
+ npm run desktop:dist:unnotarized
75
+ EOF
76
+ if [[ "${CLEMENTINE_ALLOW_UNNOTARIZED:-}" != "1" ]]; then
77
+ exit 1
78
+ fi
79
+ fi
80
+
81
+ npm run desktop:prepare
82
+ "$ROOT_DIR/node_modules/.bin/electron-builder" --mac
83
+
84
+ if [[ ${#notarytool_args[@]} -gt 0 ]]; then
85
+ package_version="$(node -p "require('./package.json').version")"
86
+ shopt -s nullglob
87
+ dmg_files=("$ROOT_DIR"/release/Clementine-"$package_version"-mac-*.dmg)
88
+ shopt -u nullglob
89
+
90
+ for dmg_file in "${dmg_files[@]}"; do
91
+ echo "Notarizing DMG wrapper: $(basename "$dmg_file")"
92
+ xcrun notarytool submit "$dmg_file" --wait "${notarytool_args[@]}"
93
+ xcrun stapler staple "$dmg_file"
94
+ xcrun stapler validate "$dmg_file"
95
+
96
+ case "$(uname -m)" in
97
+ arm64) app_builder="$ROOT_DIR/node_modules/app-builder-bin/mac/app-builder_arm64" ;;
98
+ x86_64) app_builder="$ROOT_DIR/node_modules/app-builder-bin/mac/app-builder_amd64" ;;
99
+ *) app_builder="" ;;
100
+ esac
101
+ if [[ -n "$app_builder" && -x "$app_builder" ]]; then
102
+ "$app_builder" blockmap --input "$dmg_file" --output "$dmg_file.blockmap"
103
+ fi
104
+
105
+ if [[ -f "$ROOT_DIR/release/latest-mac.yml" ]]; then
106
+ node --input-type=module - "$dmg_file" "$ROOT_DIR/release/latest-mac.yml" <<'NODE'
107
+ import { createHash } from 'node:crypto';
108
+ import { readFileSync, statSync, writeFileSync } from 'node:fs';
109
+ import { basename } from 'node:path';
110
+
111
+ const [dmgPath, latestPath] = process.argv.slice(2);
112
+ const url = basename(dmgPath);
113
+ const sha512 = createHash('sha512').update(readFileSync(dmgPath)).digest('base64');
114
+ const size = statSync(dmgPath).size;
115
+ const lines = readFileSync(latestPath, 'utf8').split(/\r?\n/);
116
+
117
+ for (let index = 0; index < lines.length; index++) {
118
+ if (lines[index].trim() === `- url: ${url}`) {
119
+ for (let cursor = index + 1; cursor < lines.length; cursor++) {
120
+ if (/^\S/.test(lines[cursor]) || /^\s*-\s+url:/.test(lines[cursor])) break;
121
+ if (lines[cursor].trim().startsWith('sha512:')) lines[cursor] = ` sha512: ${sha512}`;
122
+ if (lines[cursor].trim().startsWith('size:')) lines[cursor] = ` size: ${size}`;
123
+ }
124
+ break;
125
+ }
126
+ }
127
+
128
+ writeFileSync(latestPath, `${lines.join('\n').replace(/\n*$/, '')}\n`);
129
+ NODE
130
+ fi
131
+ done
132
+ fi