@phnx-labs/agents-cli 1.22.11 → 1.22.13

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +5 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cli.js +12 -12
  5. package/dist/commands/events.d.ts +20 -1
  6. package/dist/commands/events.js +71 -55
  7. package/dist/commands/logs.js +14 -138
  8. package/dist/commands/repo.d.ts +1 -1
  9. package/dist/commands/repo.js +3 -3
  10. package/dist/commands/sessions.d.ts +15 -0
  11. package/dist/commands/sessions.js +79 -10
  12. package/dist/commands/view.d.ts +1 -1
  13. package/dist/commands/view.js +7 -7
  14. package/dist/commands/workflows.js +1 -0
  15. package/dist/lib/cli-resources.js +2 -2
  16. package/dist/lib/event-stream.d.ts +1 -1
  17. package/dist/lib/event-stream.js +1 -1
  18. package/dist/lib/events.d.ts +21 -12
  19. package/dist/lib/events.js +312 -65
  20. package/dist/lib/exec.js +1 -2
  21. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  22. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  23. package/dist/lib/migrate.d.ts +8 -0
  24. package/dist/lib/migrate.js +26 -62
  25. package/dist/lib/resources/workflows.js +41 -21
  26. package/dist/lib/resources.d.ts +1 -1
  27. package/dist/lib/runner.js +1 -2
  28. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  29. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  30. package/dist/lib/secrets/audit.js +1 -1
  31. package/dist/lib/startup/command-registry.js +1 -1
  32. package/dist/lib/state.d.ts +1 -1
  33. package/dist/lib/state.js +60 -2
  34. package/dist/lib/watchdog/runner.d.ts +13 -0
  35. package/dist/lib/watchdog/runner.js +75 -6
  36. package/dist/lib/workflows.d.ts +22 -4
  37. package/dist/lib/workflows.js +79 -29
  38. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Centralized event logging for agents-cli.
3
3
  *
4
- * Structured JSONL audit log at ~/.agents/.history/events/events.jsonl with lossless numbered
5
- * gzip rotation at 10 MB and rich metadata for debugging/auditing.
4
+ * Structured JSONL audit logs at ~/.agents/.history/events/YYYY-MM-DD/events.jsonl with
5
+ * lossless numbered gzip rotation at 10 MiB and bounded retention.
6
6
  *
7
7
  * Features:
8
8
  * - Rich metadata: hostname, platform, arch, pid, timezone
@@ -17,7 +17,7 @@ import * as os from 'os';
17
17
  import { createHash } from 'node:crypto';
18
18
  import { gzipSync, gunzipSync } from 'node:zlib';
19
19
  import { ensureLockTarget, withFileLock } from './fs-atomic.js';
20
- import { getHistoryDir } from './state.js';
20
+ import { getUserAgentsDir } from './state.js';
21
21
  import { stampProvenance, resetEventProvenanceForTest } from './event-provenance.js';
22
22
  /** Lazy perf warehouse write — avoids a hard cycle at module load. */
23
23
  function recordPerfTiming(payload) {
@@ -47,14 +47,42 @@ function recordPerfTiming(payload) {
47
47
  // unlike _resetForTest it survives a bare reset AND propagates to CLI subprocesses
48
48
  // a test spawns, so fixture events can never land in the user's real log (#910).
49
49
  let _eventsPath;
50
- function eventsPath() {
51
- return (_eventsPath ??= process.env.AGENTS_EVENTS_PATH || path.join(getHistoryDir(), 'events', 'events.jsonl'));
50
+ let _eventsPathOverride = false;
51
+ let _legacyMigrationChecked = false;
52
+ let _userAgentsDirOverride;
53
+ function userAgentsDir() {
54
+ return _userAgentsDirOverride ?? getUserAgentsDir();
52
55
  }
53
- function eventsDir() {
54
- return path.dirname(eventsPath());
56
+ function localDateKey(date = new Date()) {
57
+ const year = date.getFullYear();
58
+ const month = String(date.getMonth() + 1).padStart(2, '0');
59
+ const day = String(date.getDate()).padStart(2, '0');
60
+ return `${year}-${month}-${day}`;
61
+ }
62
+ function eventsRoot() {
63
+ if (_eventsPathOverride && _eventsPath)
64
+ return path.dirname(_eventsPath);
65
+ return path.join(userAgentsDir(), '.history', 'events');
66
+ }
67
+ function eventsPath(date = new Date()) {
68
+ if (_eventsPathOverride && _eventsPath)
69
+ return _eventsPath;
70
+ const override = _userAgentsDirOverride ? undefined : process.env.AGENTS_EVENTS_PATH;
71
+ if (override) {
72
+ _eventsPathOverride = true;
73
+ return (_eventsPath = override);
74
+ }
75
+ return path.join(eventsRoot(), localDateKey(date), 'events.jsonl');
76
+ }
77
+ function eventsDir(date = new Date()) {
78
+ return path.dirname(eventsPath(date));
55
79
  }
56
80
  /** Default retention period in days. */
57
81
  const DEFAULT_RETENTION_DAYS = 7;
82
+ /** Default total footprint for the active log plus gzip archives (50 MiB). */
83
+ const DEFAULT_MAX_STORAGE_BYTES = 50 * 1024 * 1024;
84
+ /** Cross-process marker used to avoid a full archive scan on every append. */
85
+ const PRUNE_MARKER = '.last-prune';
58
86
  /** Default max length for truncated strings. */
59
87
  const DEFAULT_TRUNCATE_LENGTH = 500;
60
88
  /** Gzip rotation threshold in bytes (10 MB). */
@@ -154,6 +182,8 @@ function getTimezoneName() {
154
182
  }
155
183
  }
156
184
  function ensureLogsDir() {
185
+ eventsPath(); // Resolve an explicit AGENTS_EVENTS_PATH before migration checks.
186
+ migrateLegacyEventLogs();
157
187
  if (!fs.existsSync(eventsDir())) {
158
188
  fs.mkdirSync(eventsDir(), { recursive: true, mode: DIR_MODE });
159
189
  }
@@ -167,6 +197,102 @@ function ensureLogsDir() {
167
197
  }
168
198
  }
169
199
  }
200
+ /**
201
+ * Move root-level and interim flat-history event families into dated directories.
202
+ *
203
+ * The common case is a whole-family rename into an empty destination. A
204
+ * Each segment is assigned to the local calendar day of its filesystem mtime;
205
+ * new writes are split by day at source. A partially completed migration keeps
206
+ * the destination active file authoritative and assigns a fresh archive number,
207
+ * so no record is overwritten or silently discarded. The legacy active-file
208
+ * lock serializes this with older installed processes that still append there.
209
+ */
210
+ export function migrateLegacyEventLogs(userDir = userAgentsDir()) {
211
+ if (_eventsPathOverride || _legacyMigrationChecked)
212
+ return 0;
213
+ _legacyMigrationChecked = true;
214
+ const destinationRoot = path.join(userDir, '.history', 'events');
215
+ const legacyActive = path.join(userDir, 'events.jsonl');
216
+ const interimActive = path.join(destinationRoot, 'events.jsonl');
217
+ const listArchives = (dir) => {
218
+ try {
219
+ return fs.readdirSync(dir)
220
+ .map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
221
+ .filter((entry) => entry.match !== null)
222
+ .map((entry) => ({ file: entry.file, number: Number(entry.match[1]) }))
223
+ .sort((a, b) => a.number - b.number);
224
+ }
225
+ catch {
226
+ return [];
227
+ }
228
+ };
229
+ const hasSourceFiles = fs.existsSync(legacyActive) || fs.existsSync(interimActive) ||
230
+ listArchives(userDir).length > 0 || listArchives(destinationRoot).length > 0;
231
+ if (!hasSourceFiles)
232
+ return 0;
233
+ try {
234
+ fs.mkdirSync(destinationRoot, { recursive: true, mode: DIR_MODE });
235
+ ensureLockTarget(legacyActive, '', DIR_MODE);
236
+ return withFileLock(legacyActive, () => {
237
+ ensureLockTarget(interimActive, '', DIR_MODE);
238
+ return withFileLock(interimActive, () => {
239
+ const sourceFamilies = [
240
+ { dir: userDir, active: legacyActive, archives: listArchives(userDir) },
241
+ { dir: destinationRoot, active: interimActive, archives: listArchives(destinationRoot) },
242
+ ];
243
+ let moved = 0;
244
+ const nextArchiveNumber = (dir) => {
245
+ const numbers = fs.readdirSync(dir)
246
+ .map((file) => file.match(/^events\.(\d+)\.jsonl\.gz$/))
247
+ .filter((match) => match !== null)
248
+ .map((match) => Number(match[1]));
249
+ return (numbers.length ? Math.max(...numbers) : 0) + 1;
250
+ };
251
+ const withDestinationLock = (dayDir, fn) => {
252
+ const destinationActive = path.join(dayDir, 'events.jsonl');
253
+ fs.mkdirSync(dayDir, { recursive: true, mode: DIR_MODE });
254
+ ensureLockTarget(destinationActive, '', DIR_MODE);
255
+ return withFileLock(destinationActive, fn);
256
+ };
257
+ for (const family of sourceFamilies) {
258
+ const activeBytes = fs.existsSync(family.active) ? fs.statSync(family.active).size : 0;
259
+ if (activeBytes > 0) {
260
+ const activeStat = fs.statSync(family.active);
261
+ const dayDir = path.join(destinationRoot, localDateKey(activeStat.mtime));
262
+ withDestinationLock(dayDir, () => {
263
+ const archivePath = path.join(dayDir, `events.${nextArchiveNumber(dayDir)}.jsonl.gz`);
264
+ fs.writeFileSync(archivePath, gzipSync(fs.readFileSync(family.active)), { mode: FILE_MODE });
265
+ fs.utimesSync(archivePath, activeStat.atime, activeStat.mtime);
266
+ fs.truncateSync(family.active, 0);
267
+ });
268
+ moved++;
269
+ }
270
+ for (const archive of family.archives) {
271
+ const source = path.join(family.dir, archive.file);
272
+ const dayDir = path.join(destinationRoot, localDateKey(fs.statSync(source).mtime));
273
+ withDestinationLock(dayDir, () => {
274
+ const destination = path.join(dayDir, `events.${nextArchiveNumber(dayDir)}.jsonl.gz`);
275
+ fs.renameSync(source, destination);
276
+ });
277
+ moved++;
278
+ }
279
+ try {
280
+ if (fs.existsSync(family.active) && fs.statSync(family.active).size === 0)
281
+ fs.unlinkSync(family.active);
282
+ }
283
+ catch { /* an older process may have reopened it */ }
284
+ }
285
+ return moved;
286
+ });
287
+ });
288
+ }
289
+ catch {
290
+ // Audit logging must remain fail-soft. Files stay at the legacy path and a
291
+ // later process retries because this guard is process-local.
292
+ _legacyMigrationChecked = false;
293
+ return 0;
294
+ }
295
+ }
170
296
  // ─── Redaction ────────────────────────────────────────────────────────────────
171
297
  /**
172
298
  * Replace a prompt string with length + short SHA so we can correlate runs
@@ -380,7 +506,8 @@ export function emit(event, payload = {}, overrides = {}) {
380
506
  // May fail if not owner
381
507
  }
382
508
  }
383
- maybeGzipRotateLocked(logPath);
509
+ const rotated = maybeGzipRotateLocked(logPath);
510
+ maybePruneLocked(rotated);
384
511
  });
385
512
  }
386
513
  catch {
@@ -657,7 +784,7 @@ export function emitFriction(surface, failureId, payload = {}) {
657
784
  function maybeGzipRotateLocked(logPath) {
658
785
  const stat = fs.statSync(logPath);
659
786
  if (stat.size < GZIP_ROTATION_BYTES)
660
- return;
787
+ return false;
661
788
  const raw = fs.readFileSync(logPath);
662
789
  const tmpArchive = path.join(eventsDir(), `.events.1.jsonl.gz.${process.pid}.tmp`);
663
790
  fs.writeFileSync(tmpArchive, gzipSync(raw), { mode: FILE_MODE });
@@ -672,6 +799,7 @@ function maybeGzipRotateLocked(logPath) {
672
799
  }
673
800
  fs.renameSync(tmpArchive, path.join(eventsDir(), 'events.1.jsonl.gz'));
674
801
  fs.truncateSync(logPath, 0);
802
+ return true;
675
803
  }
676
804
  catch (err) {
677
805
  try {
@@ -681,45 +809,179 @@ function maybeGzipRotateLocked(logPath) {
681
809
  throw err;
682
810
  }
683
811
  }
684
- // ─── Rotation ─────────────────────────────────────────────────────────────────
685
- /**
686
- * Remove log files older than the retention period.
687
- * Removes numbered gzip archives whose filesystem mtime exceeds retention.
688
- *
689
- * @param retentionDays - Number of days to keep (default 7, from DEFAULT_RETENTION_DAYS)
690
- * @returns Number of files removed
691
- */
692
- export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
693
- try {
694
- if (!fs.existsSync(eventsDir()))
695
- return 0;
696
- const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
697
- const files = fs.readdirSync(eventsDir()).filter(f => /^events\.\d+\.jsonl\.gz$/.test(f));
698
- let removed = 0;
699
- for (const file of files) {
700
- const filePath = path.join(eventsDir(), file);
701
- if (fs.statSync(filePath).mtimeMs < cutoff) {
702
- fs.unlinkSync(filePath);
703
- removed++;
812
+ function listEventLogFiles() {
813
+ const current = eventsPath();
814
+ const files = [];
815
+ const addFile = (filePath, gzip) => {
816
+ try {
817
+ const stat = fs.statSync(filePath);
818
+ files.push({
819
+ path: filePath,
820
+ gzip,
821
+ currentActive: filePath === current,
822
+ mtimeMs: stat.mtimeMs,
823
+ size: stat.size,
824
+ });
825
+ }
826
+ catch { /* file may rotate while an unlocked reader enumerates */ }
827
+ };
828
+ const addDirectory = (dir) => {
829
+ let names = [];
830
+ try {
831
+ names = fs.readdirSync(dir);
832
+ }
833
+ catch {
834
+ return;
835
+ }
836
+ for (const name of names) {
837
+ if (name !== 'events.jsonl' && !/^events\.\d+\.jsonl\.gz$/.test(name))
838
+ continue;
839
+ addFile(path.join(dir, name), name.endsWith('.gz'));
840
+ }
841
+ };
842
+ if (_eventsPathOverride) {
843
+ if (path.basename(current) !== 'events.jsonl') {
844
+ addFile(current, false);
845
+ let names = [];
846
+ try {
847
+ names = fs.readdirSync(eventsDir());
848
+ }
849
+ catch {
850
+ return files;
851
+ }
852
+ for (const name of names) {
853
+ if (/^events\.\d+\.jsonl\.gz$/.test(name))
854
+ addFile(path.join(eventsDir(), name), true);
704
855
  }
856
+ return files;
705
857
  }
706
- return removed;
858
+ addDirectory(eventsDir());
859
+ return files;
860
+ }
861
+ let days = [];
862
+ try {
863
+ days = fs.readdirSync(eventsRoot()).filter((name) => /^\d{4}-\d{2}-\d{2}$/.test(name));
707
864
  }
708
865
  catch {
709
- return 0;
866
+ return files;
710
867
  }
868
+ for (const day of days)
869
+ addDirectory(path.join(eventsRoot(), day));
870
+ return files;
711
871
  }
712
- /**
713
- * Lazy rotation - runs at most once per day per process.
714
- */
715
- let lastRotationCheck = 0;
716
- export function maybeRotate() {
717
- const now = Date.now();
872
+ function removeEmptyDayDirectories() {
873
+ if (_eventsPathOverride)
874
+ return;
875
+ let days = [];
876
+ try {
877
+ days = fs.readdirSync(eventsRoot()).filter((name) => /^\d{4}-\d{2}-\d{2}$/.test(name));
878
+ }
879
+ catch {
880
+ return;
881
+ }
882
+ for (const day of days) {
883
+ const dir = path.join(eventsRoot(), day);
884
+ try {
885
+ if (fs.readdirSync(dir).length === 0)
886
+ fs.rmdirSync(dir);
887
+ }
888
+ catch { /* best effort */ }
889
+ }
890
+ }
891
+ function finalizePastDayLogs() {
892
+ if (_eventsPathOverride)
893
+ return;
894
+ const currentDay = localDateKey();
895
+ for (const file of listEventLogFiles()) {
896
+ if (path.basename(file.path) !== 'events.jsonl')
897
+ continue;
898
+ if (path.basename(path.dirname(file.path)) === currentDay)
899
+ continue;
900
+ try {
901
+ withFileLock(file.path, () => {
902
+ const dir = path.dirname(file.path);
903
+ const numbers = fs.readdirSync(dir)
904
+ .map((name) => name.match(/^events\.(\d+)\.jsonl\.gz$/))
905
+ .filter((match) => match !== null)
906
+ .map((match) => Number(match[1]));
907
+ const next = (numbers.length ? Math.max(...numbers) : 0) + 1;
908
+ const target = path.join(dir, `events.${next}.jsonl.gz`);
909
+ const sourceStat = fs.statSync(file.path);
910
+ fs.writeFileSync(target, gzipSync(fs.readFileSync(file.path)), { mode: FILE_MODE });
911
+ fs.utimesSync(target, sourceStat.atime, sourceStat.mtime);
912
+ fs.unlinkSync(file.path);
913
+ });
914
+ }
915
+ catch { /* retry on the next prune */ }
916
+ }
917
+ }
918
+ function pruneEventLogsLocked(retentionDays = DEFAULT_RETENTION_DAYS, maxStorageBytes = DEFAULT_MAX_STORAGE_BYTES) {
919
+ finalizePastDayLogs();
920
+ const result = { removedByAge: 0, removedBySize: 0, bytesReclaimed: 0 };
921
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
922
+ let files = listEventLogFiles();
923
+ for (const file of files) {
924
+ if (file.currentActive || file.mtimeMs >= cutoff)
925
+ continue;
926
+ try {
927
+ fs.unlinkSync(file.path);
928
+ result.removedByAge++;
929
+ result.bytesReclaimed += file.size;
930
+ }
931
+ catch { /* another process may already have removed it */ }
932
+ }
933
+ files = listEventLogFiles();
934
+ let totalBytes = files.reduce((sum, file) => sum + file.size, 0);
935
+ const oldestFirst = files
936
+ .filter((file) => !file.currentActive)
937
+ .sort((a, b) => a.mtimeMs - b.mtimeMs || a.path.localeCompare(b.path));
938
+ for (const file of oldestFirst) {
939
+ if (totalBytes <= maxStorageBytes)
940
+ break;
941
+ try {
942
+ fs.unlinkSync(file.path);
943
+ totalBytes -= file.size;
944
+ result.removedBySize++;
945
+ result.bytesReclaimed += file.size;
946
+ }
947
+ catch { /* another process may already have removed it */ }
948
+ }
949
+ removeEmptyDayDirectories();
950
+ return result;
951
+ }
952
+ /** Apply age retention and the total-size ceiling immediately. */
953
+ export function rotate(retentionDays = DEFAULT_RETENTION_DAYS, maxStorageBytes = DEFAULT_MAX_STORAGE_BYTES) {
954
+ try {
955
+ ensureLogsDir();
956
+ const active = eventsPath();
957
+ ensureLockTarget(active, '', DIR_MODE);
958
+ return withFileLock(active, () => pruneEventLogsLocked(retentionDays, maxStorageBytes));
959
+ }
960
+ catch {
961
+ return { removedByAge: 0, removedBySize: 0, bytesReclaimed: 0 };
962
+ }
963
+ }
964
+ /** Prune daily across processes, and immediately after a size rotation. */
965
+ function maybePruneLocked(force) {
966
+ const marker = path.join(eventsRoot(), PRUNE_MARKER);
718
967
  const oneDayMs = 24 * 60 * 60 * 1000;
719
- if (now - lastRotationCheck > oneDayMs) {
720
- lastRotationCheck = now;
721
- rotate();
968
+ let due = force;
969
+ try {
970
+ due ||= Date.now() - fs.statSync(marker).mtimeMs > oneDayMs;
722
971
  }
972
+ catch {
973
+ due = true;
974
+ }
975
+ if (!due)
976
+ return;
977
+ const maxBytes = force
978
+ ? DEFAULT_MAX_STORAGE_BYTES - GZIP_ROTATION_BYTES
979
+ : DEFAULT_MAX_STORAGE_BYTES;
980
+ pruneEventLogsLocked(DEFAULT_RETENTION_DAYS, maxBytes);
981
+ try {
982
+ fs.writeFileSync(marker, '', { mode: FILE_MODE });
983
+ }
984
+ catch { /* retry next append */ }
723
985
  }
724
986
  // ─── Query ────────────────────────────────────────────────────────────────────
725
987
  /**
@@ -731,19 +993,9 @@ export function maybeRotate() {
731
993
  export function query(options) {
732
994
  const { startDate, endDate = new Date(), eventTypes, level, agent, sessionId, caller, command, module, bundle, limit } = options;
733
995
  const results = [];
734
- if (!fs.existsSync(eventsDir()))
735
- return results;
736
- const files = [];
737
- if (fs.existsSync(eventsPath()))
738
- files.push({ path: eventsPath(), gzip: false });
739
- const archives = fs.readdirSync(eventsDir())
740
- .map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
741
- .filter((entry) => entry.match !== null)
742
- .map((entry) => ({ file: entry.file, number: Number(entry.match[1]) }))
743
- .sort((a, b) => a.number - b.number);
744
- for (const archive of archives) {
745
- files.push({ path: path.join(eventsDir(), archive.file), gzip: true });
746
- }
996
+ eventsPath(); // Resolve AGENTS_EVENTS_PATH before deciding whether to migrate.
997
+ migrateLegacyEventLogs();
998
+ const files = listEventLogFiles().sort((a, b) => Number(b.currentActive) - Number(a.currentActive) || b.mtimeMs - a.mtimeMs || b.path.localeCompare(a.path));
747
999
  const startMs = startDate?.getTime();
748
1000
  const endMs = endDate?.getTime();
749
1001
  for (const file of files) {
@@ -849,16 +1101,9 @@ export function stats(options = {}) {
849
1101
  let fileCount = 0;
850
1102
  let totalBytes = 0;
851
1103
  try {
852
- if (fs.existsSync(eventsDir())) {
853
- const files = fs.readdirSync(eventsDir()).filter(f => f === 'events.jsonl' || /^events\.\d+\.jsonl\.gz$/.test(f));
854
- fileCount = files.length;
855
- for (const f of files) {
856
- try {
857
- totalBytes += fs.statSync(path.join(eventsDir(), f)).size;
858
- }
859
- catch { /* skip */ }
860
- }
861
- }
1104
+ const files = listEventLogFiles();
1105
+ fileCount = files.length;
1106
+ totalBytes = files.reduce((sum, file) => sum + file.size, 0);
862
1107
  }
863
1108
  catch { /* skip */ }
864
1109
  return {
@@ -876,9 +1121,11 @@ export function stats(options = {}) {
876
1121
  export function getLogsPath() {
877
1122
  return eventsPath();
878
1123
  }
879
- export function _resetForTest(overrideEventsPath) {
1124
+ export function _resetForTest(overrideEventsPath, overrideUserAgentsDir) {
880
1125
  _eventsPath = overrideEventsPath;
1126
+ _eventsPathOverride = Boolean(overrideEventsPath);
1127
+ _userAgentsDirOverride = overrideUserAgentsDir;
1128
+ _legacyMigrationChecked = false;
881
1129
  resetEventProvenanceForTest();
882
1130
  _chmoddedPath = undefined;
883
- lastRotationCheck = 0;
884
1131
  }
package/dist/lib/exec.js CHANGED
@@ -14,7 +14,7 @@ import { parseTimeout } from './routines.js';
14
14
  import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
15
15
  import { resolveModel, buildReasoningFlags } from './models.js';
16
16
  import { isTierToken, resolveTier } from './model-tiers.js';
17
- import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
17
+ import { createTimer, redactPrompt, redactArgs } from './events.js';
18
18
  import { sanitizeProcessEnv } from './secrets/bundles.js';
19
19
  import { resolveActor, actorEnv } from './actor.js';
20
20
  import { getShimsDir, getHistoryDir, getUserAgentsDir } from './state.js';
@@ -1399,7 +1399,6 @@ async function spawnAgent(options) {
1399
1399
  const runId = launchId;
1400
1400
  options = { ...options, env: { ...options.env, AGENT_LAUNCH_ID: launchId } };
1401
1401
  const watcherState = await setupBudgetWatcher(options, cwd, runId);
1402
- maybeRotate();
1403
1402
  const timer = createTimer('agent.run', {
1404
1403
  agent: options.agent,
1405
1404
  version: options.version,
@@ -133,5 +133,13 @@ export declare function migrateRoutineDeviceToDevices(routinesDir?: string): voi
133
133
  * real routines dir (and without racing the routine module's own tests).
134
134
  */
135
135
  export declare function migrateWatchdogSentinelToRoutine(sentinelPath?: string, ensure?: (enabled: boolean) => void): void;
136
+ /**
137
+ * Rename cli/ → clis/ in each of the given `.agents/` directories.
138
+ *
139
+ * One-way, idempotent: no-op when src is absent. Throws when both
140
+ * `<dir>/cli` and `<dir>/clis` exist — the user must resolve the conflict
141
+ * manually before proceeding. Exported for unit-testing with temp dirs.
142
+ */
143
+ export declare function migrateCliDirToClis(agentsDirs: string[]): void;
136
144
  /** Run all idempotent migrations. Safe to call multiple times. */
137
145
  export declare function runMigration(): Promise<void>;
@@ -998,7 +998,6 @@ function migrateRuntimeToHistory() {
998
998
  moveDirOnce(path.join(USER_DIR, '.backups'), path.join(HISTORY_DIR, 'backups'));
999
999
  moveDirOnce(path.join(USER_DIR, 'routines', 'runs'), path.join(HISTORY_DIR, 'runs'));
1000
1000
  moveDirOnce(path.join(USER_DIR, 'teams', 'agents'), path.join(HISTORY_DIR, 'teams', 'agents'));
1001
- migrateEventLogsToHistory();
1002
1001
  // Drop any empty leftover skeletons created mid-rename (e.g. `versions/<agent>/<v>/home/`
1003
1002
  // recreated by a concurrent process). The real data is already under .history/.
1004
1003
  rmEmptyDirTree(path.join(USER_DIR, 'versions'));
@@ -1014,66 +1013,6 @@ function migrateRuntimeToHistory() {
1014
1013
  catch { /* best-effort */ }
1015
1014
  }
1016
1015
  }
1017
- /** Move the operational event stream out of the git-backed user-repo root. */
1018
- function migrateEventLogsToHistory() {
1019
- const destination = path.join(HISTORY_DIR, 'events');
1020
- let files = [];
1021
- try {
1022
- files = fs.readdirSync(USER_DIR).filter((file) => file === 'events.jsonl' || /^events\.\d+\.jsonl\.gz$/.test(file));
1023
- }
1024
- catch {
1025
- return;
1026
- }
1027
- try {
1028
- fs.mkdirSync(destination, { recursive: true, mode: 0o700 });
1029
- }
1030
- catch {
1031
- return;
1032
- }
1033
- const active = files.find((file) => file === 'events.jsonl');
1034
- if (active) {
1035
- const src = path.join(USER_DIR, active);
1036
- const dest = path.join(destination, active);
1037
- if (!fs.existsSync(dest)) {
1038
- moveFileOnce(src, dest);
1039
- }
1040
- else {
1041
- try {
1042
- const lines = [fs.readFileSync(src, 'utf-8'), fs.readFileSync(dest, 'utf-8')]
1043
- .flatMap((content) => content.split('\n').filter(Boolean))
1044
- .map((line, index) => {
1045
- try {
1046
- const timestamp = Date.parse(JSON.parse(line).ts ?? '');
1047
- return { line, index, timestamp: Number.isNaN(timestamp) ? Number.MAX_SAFE_INTEGER : timestamp };
1048
- }
1049
- catch {
1050
- return { line, index, timestamp: Number.MAX_SAFE_INTEGER };
1051
- }
1052
- })
1053
- .sort((a, b) => a.timestamp - b.timestamp || a.index - b.index)
1054
- .map(({ line }) => line);
1055
- atomicWriteFileSync(dest, `${lines.join('\n')}\n`, { mode: 0o600 });
1056
- fs.unlinkSync(src);
1057
- }
1058
- catch { /* preserve the source for a later retry */ }
1059
- }
1060
- }
1061
- const archives = files
1062
- .map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
1063
- .filter((entry) => entry.match !== null)
1064
- .sort((a, b) => Number(a.match[1]) - Number(b.match[1]));
1065
- let nextArchive = fs.readdirSync(destination).reduce((max, file) => {
1066
- const match = file.match(/^events\.(\d+)\.jsonl\.gz$/);
1067
- return match ? Math.max(max, Number(match[1])) : max;
1068
- }, 0) + 1;
1069
- for (const archive of archives) {
1070
- const preferred = path.join(destination, archive.file);
1071
- const dest = fs.existsSync(preferred)
1072
- ? path.join(destination, `events.${nextArchive++}.jsonl.gz`)
1073
- : preferred;
1074
- moveFileOnce(path.join(USER_DIR, archive.file), dest);
1075
- }
1076
- }
1077
1016
  /**
1078
1017
  * Restore plugins from the cache bucket back to the user-root.
1079
1018
  *
@@ -1694,7 +1633,7 @@ function containsOnlyDsStore(dir) {
1694
1633
  function warnSystemOrphans() {
1695
1634
  const SHIPPED_ALLOWLIST = new Set([
1696
1635
  // resource directories shipped by the npm package
1697
- 'commands', 'hooks', 'skills', 'rules', 'mcp', 'cli', 'permissions', 'subagents', 'profiles', 'agents', 'routines',
1636
+ 'commands', 'hooks', 'skills', 'rules', 'mcp', 'clis', 'permissions', 'subagents', 'profiles', 'agents', 'routines',
1698
1637
  // top-level metadata files
1699
1638
  'agents.yaml', 'hooks.yaml', 'README.md', 'CHANGELOG.md',
1700
1639
  // git + repo metadata
@@ -2074,10 +2013,35 @@ export function migrateWatchdogSentinelToRoutine(sentinelPath = path.join(CACHE_
2074
2013
  catch { /* already gone */ }
2075
2014
  console.error('Migrated watchdog: legacy enable sentinel → watchdog routine (kept enabled)');
2076
2015
  }
2016
+ /**
2017
+ * Rename cli/ → clis/ in each of the given `.agents/` directories.
2018
+ *
2019
+ * One-way, idempotent: no-op when src is absent. Throws when both
2020
+ * `<dir>/cli` and `<dir>/clis` exist — the user must resolve the conflict
2021
+ * manually before proceeding. Exported for unit-testing with temp dirs.
2022
+ */
2023
+ export function migrateCliDirToClis(agentsDirs) {
2024
+ for (const agentsDir of agentsDirs) {
2025
+ const src = path.join(agentsDir, 'cli');
2026
+ const dest = path.join(agentsDir, 'clis');
2027
+ if (!fs.existsSync(src))
2028
+ continue;
2029
+ if (fs.existsSync(dest)) {
2030
+ throw new Error(`Migration conflict: both ${src} and ${dest} exist. ` +
2031
+ `Remove or merge the old cli/ directory into clis/ manually.`);
2032
+ }
2033
+ fs.renameSync(src, dest);
2034
+ }
2035
+ }
2077
2036
  /** Run all idempotent migrations. Safe to call multiple times. */
2078
2037
  export async function runMigration() {
2079
2038
  // MUST run first: every other migrator reads SYSTEM_DIR (the new path).
2080
2039
  foldLegacySystemRepo();
2040
+ const cliMigrateDirs = [USER_DIR, SYSTEM_DIR];
2041
+ const projectDotAgents = path.join(process.cwd(), '.agents');
2042
+ if (fs.existsSync(projectDotAgents))
2043
+ cliMigrateDirs.push(projectDotAgents);
2044
+ migrateCliDirToClis(cliMigrateDirs);
2081
2045
  migrateAgentsYaml();
2082
2046
  deleteSystemPromptsJson();
2083
2047
  migrateSystemConfigJson();