clementine-agent 1.0.24 → 1.0.26

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.
@@ -11,8 +11,10 @@
11
11
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
12
12
  import path from 'node:path';
13
13
  import pino from 'pino';
14
- import { GOALS_DIR, BASE_DIR } from '../config.js';
14
+ import { GOALS_DIR, BASE_DIR, MEMORY_DB_PATH, VAULT_DIR } from '../config.js';
15
15
  import { listAllGoals } from '../tools/shared.js';
16
+ import { computeBrokenJobs } from '../gateway/failure-monitor.js';
17
+ import { MemoryStore } from '../memory/store.js';
16
18
  const logger = pino({ name: 'clementine.insight-engine' });
17
19
  const BASE_COOLDOWN_MS = 2 * 60 * 60 * 1000; // 2 hours
18
20
  const MAX_DAILY_INSIGHTS = 3;
@@ -172,9 +174,7 @@ export function gatherInsightSignals(gateway) {
172
174
  // with a diagnosis is a real, actionable signal the owner should
173
175
  // see proactively rather than stumble across in the dashboard.
174
176
  try {
175
- // eslint-disable-next-line @typescript-eslint/no-require-imports
176
- const fm = require('../gateway/failure-monitor.js');
177
- const broken = fm.computeBrokenJobs();
177
+ const broken = computeBrokenJobs();
178
178
  for (const b of broken.slice(0, 3)) {
179
179
  const hint = b.diagnosis?.rootCause
180
180
  ? ` — ${b.diagnosis.rootCause.slice(0, 120)}`
@@ -185,14 +185,13 @@ export function gatherInsightSignals(gateway) {
185
185
  }
186
186
  }
187
187
  }
188
- catch { /* failure-monitor may not be loadable; fine */ }
188
+ catch (err) {
189
+ logger.debug({ err }, 'Failed to pull broken-jobs signals');
190
+ }
189
191
  // 6. Claim tracker — failed claims in the last N hours erode trust.
190
192
  // Surface them so the owner sees "Clementine said she'd do X; she
191
193
  // didn't" instead of silently swallowing the miss.
192
194
  try {
193
- // eslint-disable-next-line @typescript-eslint/no-require-imports
194
- const { MemoryStore } = require('../memory/store.js');
195
- const { MEMORY_DB_PATH, VAULT_DIR } = require('../config.js');
196
195
  if (existsSync(MEMORY_DB_PATH)) {
197
196
  const store = new MemoryStore(MEMORY_DB_PATH, VAULT_DIR);
198
197
  store.initialize();
@@ -209,7 +208,9 @@ export function gatherInsightSignals(gateway) {
209
208
  store.close();
210
209
  }
211
210
  }
212
- catch { /* non-fatal */ }
211
+ catch (err) {
212
+ logger.debug({ err }, 'Failed to pull failed-claims signals');
213
+ }
213
214
  return signals;
214
215
  }
215
216
  /**
@@ -10,7 +10,7 @@
10
10
  * so the response to a silent failure is "here's what's wrong and
11
11
  * here's what to change" rather than "go investigate."
12
12
  */
13
- import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
13
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import pino from 'pino';
16
16
  import { AGENTS_DIR, BASE_DIR, CRON_FILE } from '../config.js';
@@ -54,7 +54,6 @@ function saveCache(cache) {
54
54
  mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
55
55
  const tmp = CACHE_FILE + '.tmp';
56
56
  writeFileSync(tmp, JSON.stringify(cache, null, 2));
57
- const { renameSync } = require('node:fs');
58
57
  renameSync(tmp, CACHE_FILE);
59
58
  }
60
59
  catch (err) {
@@ -15,10 +15,11 @@
15
15
  * Read-only with respect to the cron run logs and advisor events; mutates
16
16
  * only its own state file (cron/failure-monitor.json).
17
17
  */
18
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from 'node:fs';
18
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from 'node:fs';
19
19
  import path from 'node:path';
20
+ import Database from 'better-sqlite3';
20
21
  import pino from 'pino';
21
- import { BASE_DIR } from '../config.js';
22
+ import { BASE_DIR, MEMORY_DB_PATH } from '../config.js';
22
23
  const logger = pino({ name: 'clementine.failure-monitor' });
23
24
  const RUNS_DIR = path.join(BASE_DIR, 'cron', 'runs');
24
25
  const ADVISOR_EVENTS_FILE = path.join(BASE_DIR, 'cron', 'advisor-events.jsonl');
@@ -54,7 +55,6 @@ function saveState(state) {
54
55
  // writes from corrupting the state if the process is killed mid-write.
55
56
  const tmp = STATE_FILE + '.tmp';
56
57
  writeFileSync(tmp, JSON.stringify(state, null, 2));
57
- const { renameSync } = require('node:fs');
58
58
  renameSync(tmp, STATE_FILE);
59
59
  }
60
60
  catch (err) {
@@ -105,11 +105,8 @@ function isFailure(entry, gradeCache) {
105
105
  function loadGradeCache() {
106
106
  const cache = new Map();
107
107
  try {
108
- const { MEMORY_DB_PATH } = require('../config.js');
109
108
  if (!existsSync(MEMORY_DB_PATH))
110
109
  return cache;
111
- // eslint-disable-next-line @typescript-eslint/no-require-imports
112
- const Database = require('better-sqlite3');
113
110
  const db = new Database(MEMORY_DB_PATH, { readonly: true });
114
111
  try {
115
112
  const rows = db.prepare(`SELECT job_name, started_at, passed FROM graded_runs
@@ -121,7 +118,9 @@ function loadGradeCache() {
121
118
  catch { /* graded_runs may not exist on older DBs */ }
122
119
  db.close();
123
120
  }
124
- catch { /* non-fatal */ }
121
+ catch (err) {
122
+ logger.warn({ err }, 'Failed to load grade cache');
123
+ }
125
124
  return cache;
126
125
  }
127
126
  /**
@@ -7,7 +7,7 @@
7
7
  * owner with the verdict — succeeded or still failing — so a self-reported
8
8
  * "fix" can't go untested again.
9
9
  */
10
- import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
10
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import crypto from 'node:crypto';
13
13
  import pino from 'pino';
@@ -31,7 +31,6 @@ function saveState(state) {
31
31
  mkdirSync(path.dirname(STATE_FILE), { recursive: true });
32
32
  const tmp = STATE_FILE + '.tmp';
33
33
  writeFileSync(tmp, JSON.stringify(state, null, 2));
34
- const { renameSync } = require('node:fs');
35
34
  renameSync(tmp, STATE_FILE);
36
35
  }
37
36
  catch (err) {
@@ -5,7 +5,7 @@
5
5
  * Manages per-user/channel sessions for conversation continuity.
6
6
  */
7
7
  import path from 'node:path';
8
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
9
9
  import pino from 'pino';
10
10
  import { PersonalAssistant } from '../agent/assistant.js';
11
11
  import { SelfImproveLoop } from '../agent/self-improve.js';
@@ -743,7 +743,15 @@ export class Gateway {
743
743
  // specific agent profile, classify whether the message should go
744
744
  // to a specialist. Direct-to-agent-bot sessions bypass this entirely.
745
745
  // Small-talk and meta queries stay with Clementine by default.
746
- const routingResult = !isInternalMsg && !sess?.profile && !text.startsWith('!')
746
+ //
747
+ // Also bypass structured workflow messages — button clicks, approvals,
748
+ // and other system-injected interactions are not free-form chat and
749
+ // shouldn't be reclassified. They already have an intended flow.
750
+ const isStructuredWorkflowMsg = text.startsWith('[Button clicked:')
751
+ || text.startsWith('[Approval:')
752
+ || text.startsWith('[Reaction:')
753
+ || text.startsWith('[System:');
754
+ const routingResult = !isInternalMsg && !sess?.profile && !text.startsWith('!') && !isStructuredWorkflowMsg
747
755
  ? await this._maybeRouteToSpecialist(sessionKey, text, onText)
748
756
  : null;
749
757
  if (routingResult?.delegated) {
@@ -956,12 +964,36 @@ export class Gateway {
956
964
  }
957
965
  }
958
966
  // ── Deep mode detection ─────────────────────────────────────
959
- // Agent proposes background execution for complex tasks
960
- const deepMatch = response?.match(/^\[DEEP_MODE:\s*(.+?)\]\s*/s);
967
+ // Agent proposes background execution for complex tasks.
968
+ //
969
+ // Syntax:
970
+ // [DEEP_MODE: task description]
971
+ // [DEEP_MODE(work_dir=/abs/path): task description] ← NEW
972
+ //
973
+ // The optional work_dir= parameter runs the unleashed task in a
974
+ // different project directory. Useful when the task belongs to
975
+ // a Claude Code project with its own CLAUDE.md / slash commands
976
+ // (e.g. the proposal-builder project for audit-queue approvals).
977
+ const deepMatch = response?.match(/^\[DEEP_MODE(?:\(([^)]+)\))?:\s*(.+?)\]\s*/s);
961
978
  if (deepMatch) {
962
- const taskDesc = deepMatch[1].trim() || text;
963
- const ack = response.replace(/^\[DEEP_MODE:[^\]]*\]\s*/s, '').trim();
979
+ const paramsStr = deepMatch[1] ?? '';
980
+ const taskDesc = deepMatch[2].trim() || text;
981
+ const ack = response.replace(/^\[DEEP_MODE(?:\([^)]*\))?:[^\]]*\]\s*/s, '').trim();
964
982
  logger.info({ sessionKey, task: taskDesc }, 'Deep mode triggered by agent');
983
+ // Parse optional work_dir parameter — strict: must be an absolute
984
+ // path and must exist. Anything else falls back to default.
985
+ let deepWorkDir;
986
+ const wdMatch = paramsStr.match(/work_dir=([^,\s]+)/);
987
+ if (wdMatch) {
988
+ const candidate = wdMatch[1].trim().replace(/^["']|["']$/g, '');
989
+ if (path.isAbsolute(candidate) && existsSync(candidate)) {
990
+ deepWorkDir = candidate;
991
+ logger.info({ sessionKey, workDir: deepWorkDir }, 'Deep mode using custom work_dir');
992
+ }
993
+ else {
994
+ logger.warn({ sessionKey, candidate }, 'Deep mode work_dir rejected (not absolute or does not exist)');
995
+ }
996
+ }
965
997
  const currentSess = this.getSession(sessionKey);
966
998
  const jobName = `deep-${Date.now()}`;
967
999
  currentSess.deepTask = { jobName, taskDesc, startedAt: new Date().toISOString() };
@@ -969,7 +1001,7 @@ export class Gateway {
969
1001
  this.assistant.runUnleashedTask(jobName, `${taskDesc}\n\nOriginal request: ${text}`, 2, // tier 2 (Bash/Write/Edit enabled)
970
1002
  undefined, // default maxTurns (75/phase)
971
1003
  undefined, // default model
972
- undefined, // default workDir
1004
+ deepWorkDir, // honors [DEEP_MODE(work_dir=...)] if provided
973
1005
  1).then(async (result) => {
974
1006
  logger.info({ sessionKey, jobName, resultLen: result?.length ?? 0 }, 'Deep mode task completed');
975
1007
  if (result && result !== '__NOTHING__') {
@@ -1511,8 +1543,6 @@ function logRouteDecision(opts) {
1511
1543
  while (_routeAuditBuffer.length > 200)
1512
1544
  _routeAuditBuffer.shift();
1513
1545
  try {
1514
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1515
- const { appendFileSync } = require('node:fs');
1516
1546
  appendFileSync(Gateway.routeAuditLogPath(), JSON.stringify(entry) + '\n');
1517
1547
  }
1518
1548
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",