@phnx-labs/agents-cli 1.22.32 → 1.22.34

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 (76) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +2 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/accounts.js +3 -3
  5. package/dist/commands/browser-sessions-picker.d.ts +16 -0
  6. package/dist/commands/browser-sessions-picker.js +179 -0
  7. package/dist/commands/browser.js +9 -4
  8. package/dist/commands/hosts.js +1 -5
  9. package/dist/commands/inspect.js +174 -41
  10. package/dist/commands/message.d.ts +6 -1
  11. package/dist/commands/message.js +60 -3
  12. package/dist/commands/sessions-browser.js +2 -2
  13. package/dist/commands/sessions.d.ts +54 -4
  14. package/dist/commands/sessions.js +274 -58
  15. package/dist/commands/ssh.js +17 -5
  16. package/dist/commands/teams.d.ts +28 -0
  17. package/dist/commands/teams.js +148 -13
  18. package/dist/commands/upgrade.d.ts +7 -0
  19. package/dist/commands/upgrade.js +10 -0
  20. package/dist/commands/watchdog.d.ts +2 -0
  21. package/dist/commands/watchdog.js +112 -27
  22. package/dist/index.js +51 -59
  23. package/dist/lib/agents.js +6 -0
  24. package/dist/lib/browser/sessions-list.d.ts +81 -0
  25. package/dist/lib/browser/sessions-list.js +179 -4
  26. package/dist/lib/daemon.js +45 -5
  27. package/dist/lib/devices/connect.d.ts +33 -0
  28. package/dist/lib/devices/connect.js +61 -3
  29. package/dist/lib/devices/doctor-findings.d.ts +4 -2
  30. package/dist/lib/devices/doctor-findings.js +4 -2
  31. package/dist/lib/exec.js +63 -6
  32. package/dist/lib/help.d.ts +3 -2
  33. package/dist/lib/help.js +4 -0
  34. package/dist/lib/hosts/dispatch.d.ts +2 -32
  35. package/dist/lib/hosts/dispatch.js +6 -61
  36. package/dist/lib/hosts/tasks.d.ts +7 -0
  37. package/dist/lib/hosts/tasks.js +9 -0
  38. package/dist/lib/mailbox-target.d.ts +27 -0
  39. package/dist/lib/mailbox-target.js +21 -0
  40. package/dist/lib/mcp.d.ts +10 -0
  41. package/dist/lib/mcp.js +21 -2
  42. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  43. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  44. package/dist/lib/migrate.d.ts +11 -0
  45. package/dist/lib/migrate.js +40 -0
  46. package/dist/lib/project-root.d.ts +47 -0
  47. package/dist/lib/project-root.js +68 -0
  48. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  49. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  50. package/dist/lib/secrets/agent.d.ts +9 -2
  51. package/dist/lib/secrets/agent.js +52 -7
  52. package/dist/lib/secrets/reaper.d.ts +24 -3
  53. package/dist/lib/secrets/reaper.js +55 -6
  54. package/dist/lib/session/discover.js +12 -0
  55. package/dist/lib/session/render.d.ts +2 -0
  56. package/dist/lib/session/render.js +1 -1
  57. package/dist/lib/session/team-filter.d.ts +11 -0
  58. package/dist/lib/session/team-filter.js +10 -0
  59. package/dist/lib/shims.js +9 -2
  60. package/dist/lib/startup/command-registry.d.ts +1 -0
  61. package/dist/lib/startup/command-registry.js +8 -3
  62. package/dist/lib/startup/root-command.d.ts +3 -0
  63. package/dist/lib/startup/root-command.js +10 -0
  64. package/dist/lib/teams/agents.d.ts +136 -6
  65. package/dist/lib/teams/agents.js +324 -58
  66. package/dist/lib/teams/worktree.d.ts +39 -2
  67. package/dist/lib/teams/worktree.js +60 -4
  68. package/dist/lib/types.d.ts +11 -0
  69. package/dist/lib/versions.js +2 -2
  70. package/dist/lib/watchdog/history.d.ts +20 -0
  71. package/dist/lib/watchdog/history.js +46 -0
  72. package/dist/lib/watchdog/log.d.ts +16 -1
  73. package/dist/lib/watchdog/log.js +82 -2
  74. package/dist/lib/watchdog/runner.d.ts +12 -0
  75. package/dist/lib/watchdog/runner.js +20 -0
  76. package/package.json +1 -1
@@ -9,6 +9,7 @@ import { promisify } from 'util';
9
9
  import * as fs from 'fs/promises';
10
10
  import * as path from 'path';
11
11
  import { safeJoin } from '../paths.js';
12
+ import { getMainRepoRoot } from '../git.js';
12
13
  const execFileAsync = promisify(execFile);
13
14
  const WORKTREE_NAME_RE = /^[A-Za-z0-9_-]+$/;
14
15
  /**
@@ -106,7 +107,16 @@ export async function localDefaultBranch(gitRoot) {
106
107
  * code. Matches `createRemoteWorktree` — local and remote isolation share one
107
108
  * base policy.
108
109
  *
109
- * @param repoDir - Directory inside the git repository
110
+ * Resolves the placement root with {@link getMainRepoRoot}, NOT the local
111
+ * `getGitRoot`/`--show-toplevel`, on purpose: when `repoDir` (the caller's
112
+ * ambient cwd) is itself inside another teammate's linked worktree,
113
+ * `--show-toplevel` returns THAT worktree's own root, so the new worktree
114
+ * lands nested inside it (observed: `.../worktrees/A/.agents/worktrees/B`,
115
+ * destroyed along with A's cleanup). `getMainRepoRoot` follows
116
+ * `--git-common-dir`, which always points at the primary checkout's `.git`
117
+ * regardless of which worktree the caller is standing in.
118
+ *
119
+ * @param repoDir - Directory inside the git repository (main checkout or any linked worktree)
110
120
  * @param worktreeName - Name for the worktree (used in path and branch)
111
121
  * @returns The absolute path to the created worktree
112
122
  */
@@ -114,7 +124,7 @@ export async function createWorktree(repoDir, worktreeName) {
114
124
  if (!WORKTREE_NAME_RE.test(worktreeName)) {
115
125
  throw new Error(`Invalid worktree name: ${worktreeName}`);
116
126
  }
117
- const gitRoot = await getGitRoot(repoDir);
127
+ const gitRoot = await getMainRepoRoot(repoDir);
118
128
  const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
119
129
  const branchName = `agents/${worktreeName}`;
120
130
  const base = await localDefaultBranch(gitRoot);
@@ -137,7 +147,12 @@ export async function createWorktree(repoDir, worktreeName) {
137
147
  /**
138
148
  * Remove a git worktree and optionally its branch.
139
149
  *
140
- * @param repoDir - Directory inside the main git repository (not the worktree)
150
+ * Resolves via {@link getMainRepoRoot}, same as {@link createWorktree} a
151
+ * caller standing inside a DIFFERENT linked worktree (e.g. a `teams stop`
152
+ * run from within another teammate's checkout) must still target the main
153
+ * repo's `.agents/worktrees/<name>`, not `--show-toplevel`'s local answer.
154
+ *
155
+ * @param repoDir - Directory inside the git repository (main checkout or any linked worktree)
141
156
  * @param worktreeName - Name of the worktree to remove
142
157
  * @param deleteBranch - Whether to delete the associated branch
143
158
  */
@@ -145,7 +160,7 @@ export async function removeWorktree(repoDir, worktreeName, deleteBranch = true)
145
160
  if (!WORKTREE_NAME_RE.test(worktreeName)) {
146
161
  throw new Error(`Invalid worktree name: ${worktreeName}`);
147
162
  }
148
- const gitRoot = await getGitRoot(repoDir);
163
+ const gitRoot = await getMainRepoRoot(repoDir);
149
164
  const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
150
165
  const branchName = `agents/${worktreeName}`;
151
166
  try {
@@ -183,3 +198,44 @@ export function getWorktreePath(gitRoot, worktreeName) {
183
198
  export function getWorktreeBranch(worktreeName) {
184
199
  return `agents/${worktreeName}`;
185
200
  }
201
+ /**
202
+ * Does the CHECKOUT DIRECTORY for this worktree name exist?
203
+ *
204
+ * Narrower than {@link worktreeExists}, and the difference is load-bearing:
205
+ * `teams add` only ever cleans up after a failed create when there is NO
206
+ * checkout — i.e. when all that can be left is a dangling `agents/<name>`
207
+ * branch ref. That keeps the cleanup incapable of deleting anybody's files,
208
+ * including a concurrent add's freshly-created worktree, whatever the
209
+ * pre-flight probe saw a `git fetch` ago. (RUSH-2356)
210
+ */
211
+ export async function worktreeCheckoutExists(repoDir, worktreeName) {
212
+ if (!WORKTREE_NAME_RE.test(worktreeName)) {
213
+ throw new Error(`Invalid worktree name: ${worktreeName}`);
214
+ }
215
+ const gitRoot = await getMainRepoRoot(repoDir);
216
+ const dir = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
217
+ return fs.stat(dir).then(() => true).catch(() => false);
218
+ }
219
+ /**
220
+ * Does anything already exist under this worktree name — the checkout directory
221
+ * or its `agents/<name>` branch?
222
+ *
223
+ * Answered from git and the filesystem, never from teammate records: it is asked
224
+ * by `teams add` immediately BEFORE {@link createWorktree}, so that if the create
225
+ * fails the command can tell "the branch ref I half-created" (safe to remove)
226
+ * from "something that was already here" (never remove — `teams stop` deliberately
227
+ * KEEPS a worktree holding uncommitted changes, and its teammate record is
228
+ * terminal by then, so no record-based check can protect it). (RUSH-2356)
229
+ */
230
+ export async function worktreeExists(repoDir, worktreeName) {
231
+ if (await worktreeCheckoutExists(repoDir, worktreeName))
232
+ return true;
233
+ const gitRoot = await getMainRepoRoot(repoDir);
234
+ try {
235
+ await execFileAsync('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${getWorktreeBranch(worktreeName)}`], { cwd: gitRoot });
236
+ return true;
237
+ }
238
+ catch {
239
+ return false; // `--verify` exits non-zero when the ref doesn't exist
240
+ }
241
+ }
@@ -587,6 +587,17 @@ export interface PluginManifest {
587
587
  description: string;
588
588
  version: string;
589
589
  agents?: AgentId[];
590
+ /**
591
+ * Who published the plugin, per the official plugin format. Every plugin.json
592
+ * in this repo already carries one; it was missing from this interface, so
593
+ * `loadPluginManifest`'s cast passed it through un-typed and no surface read it.
594
+ * Accepts the shorthand string form as well as the object form.
595
+ */
596
+ author?: string | {
597
+ name: string;
598
+ email?: string;
599
+ url?: string;
600
+ };
590
601
  /** Interactive config fields prompted at install time. Values stored in .user-config.json. */
591
602
  userConfig?: PluginUserConfigField[];
592
603
  /** Other plugin names this plugin depends on. Missing deps produce a warning. */
@@ -31,7 +31,7 @@ import { activeRulesPreset, filterNamesForActiveResourceProfile } from './resour
31
31
  import { VERSION_RE, compareVersions } from './agent-spec/primitives.js';
32
32
  import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath, isSelfUpdatingAgent, isAgentHardDeprecated, hardDeprecationError } from './agents.js';
33
33
  import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
34
- import { parseMcpServerConfig, isProjectMcpTrusted } from './mcp.js';
34
+ import { parseMcpConfigForScan, isProjectMcpTrusted } from './mcp.js';
35
35
  import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink, assertIsolationBoundary, } from './shims.js';
36
36
  import { importInstallScriptBinary } from './import.js';
37
37
  import { createInstallation } from './installations/store.js';
@@ -84,7 +84,7 @@ function getScopedMcpResources(cwd) {
84
84
  const files = fs.readdirSync(mcpDir)
85
85
  .filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
86
86
  for (const file of files) {
87
- const config = parseMcpServerConfig(path.join(mcpDir, file));
87
+ const config = parseMcpConfigForScan(path.join(mcpDir, file));
88
88
  if (config?.name && !resources.has(config.name)) {
89
89
  resources.set(config.name, { name: config.name, scope });
90
90
  }
@@ -0,0 +1,20 @@
1
+ import type { WatchdogEvent, WatchdogEventKind } from './log.js';
2
+ export interface WatchdogHistoryEntry {
3
+ ts: number;
4
+ kind: WatchdogEventKind | 'inspection';
5
+ sessionId?: string;
6
+ agent?: string;
7
+ message: string;
8
+ reason?: string;
9
+ stalledForMs?: number;
10
+ nudgeText?: string;
11
+ }
12
+ export interface WatchdogHistoryOptions {
13
+ limit?: number;
14
+ sinceMs?: number;
15
+ sessionId?: string;
16
+ includeTicks?: boolean;
17
+ nowMs?: number;
18
+ }
19
+ /** Select newest history and deliberately remove raw transcript tailLines. */
20
+ export declare function selectWatchdogHistory(events: WatchdogEvent[], options?: WatchdogHistoryOptions): WatchdogHistoryEntry[];
@@ -0,0 +1,46 @@
1
+ /** Select newest history and deliberately remove raw transcript tailLines. */
2
+ export function selectWatchdogHistory(events, options = {}) {
3
+ const limit = options.limit ?? 50;
4
+ const cutoff = options.sinceMs === undefined
5
+ ? Number.NEGATIVE_INFINITY
6
+ : (options.nowMs ?? Date.now()) - options.sinceMs;
7
+ const session = options.sessionId?.toLowerCase();
8
+ const expanded = events.flatMap((event) => {
9
+ const inspections = event.inspections?.map((inspection) => ({
10
+ ts: event.ts,
11
+ kind: 'inspection',
12
+ sessionId: inspection.terminalId,
13
+ agent: inspection.agentType,
14
+ message: inspection.message,
15
+ reason: inspection.reason,
16
+ stalledForMs: inspection.stalledForMs,
17
+ })) ?? [];
18
+ if (event.kind === 'tick' && !options.includeTicks)
19
+ return inspections;
20
+ return [...inspections, {
21
+ ts: event.ts,
22
+ kind: event.kind,
23
+ sessionId: event.terminalId,
24
+ agent: event.agentType,
25
+ message: event.message,
26
+ reason: event.reason,
27
+ stalledForMs: event.stalledForMs,
28
+ nudgeText: event.nudgeText,
29
+ }];
30
+ });
31
+ const matching = expanded
32
+ .filter((entry) => entry.ts >= cutoff)
33
+ .filter((entry) => !session || entry.sessionId?.toLowerCase().startsWith(session))
34
+ .sort((a, b) => b.ts - a.ts);
35
+ if (matching.length <= limit)
36
+ return matching;
37
+ // A busy tick can contain more inspections than the display limit. Keep the
38
+ // newest timeline, but reserve one row for the newest real action.
39
+ const newest = matching.slice(0, limit);
40
+ if (newest.some((entry) => entry.kind !== 'inspection'))
41
+ return newest;
42
+ const newestAction = matching.find((entry) => entry.kind !== 'inspection');
43
+ if (!newestAction)
44
+ return newest;
45
+ return [...newest.slice(0, limit - 1), newestAction].sort((a, b) => b.ts - a.ts);
46
+ }
@@ -19,7 +19,19 @@ export interface WatchdogEvent {
19
19
  lastAssistantMessage?: string;
20
20
  /** For a 'decision'/'nudge' that injects: the exact text delivered. */
21
21
  nudgeText?: string;
22
+ /** Compact per-session evaluations carried by heartbeat ticks. */
23
+ inspections?: WatchdogInspection[];
22
24
  }
25
+ export interface WatchdogInspection {
26
+ terminalId?: string;
27
+ agentType: string;
28
+ message: string;
29
+ reason: string;
30
+ stalledForMs?: number;
31
+ }
32
+ /** Parse the audit log without letting a partial final append hide valid rows. */
33
+ export declare function parseWatchdogEvents(text: string): WatchdogEvent[];
34
+ export declare function readWatchdogEvents(logPath?: string): WatchdogEvent[];
23
35
  /** Serialize one event to a JSONL line (no trailing newline). */
24
36
  export declare function formatEvent(ev: WatchdogEvent): string;
25
37
  /**
@@ -27,7 +39,10 @@ export declare function formatEvent(ev: WatchdogEvent): string;
27
39
  * writer trims (watchdogLog.ts trimToLast). Large enough to keep a useful
28
40
  * history, small enough to bound the file the UI polls.
29
41
  */
30
- export declare const WATCHDOG_LOG_MAX_LINES = 2000;
42
+ export declare const WATCHDOG_LOG_MAX_LINES = 5000;
43
+ export declare const WATCHDOG_TAIL_MAX_CHARS = 4096;
44
+ /** Keep the newest transcript context without letting it consume the audit window. */
45
+ export declare function boundTailLines(lines: string[], maxChars?: number): string[];
31
46
  /** Trim a JSONL body to the last `maxLines` events (same logic as the reader). */
32
47
  export declare function trimToLast(text: string, maxLines: number): string;
33
48
  /**
@@ -18,6 +18,65 @@ import * as path from 'path';
18
18
  import { withFileLock, atomicWriteFileSync, ensureLockTarget } from '../fs-atomic.js';
19
19
  /** Same path the Factory reader pulls (apps/factory/src/core/watchdogLog.ts). */
20
20
  export const WATCHDOG_LOG_PATH = path.join(os.homedir(), '.agents', '.cache', 'logs', 'watchdog.log');
21
+ const WATCHDOG_EVENT_KINDS = new Set(['tick', 'decision', 'nudge', 'rotate', 'error']);
22
+ /** Parse the audit log without letting a partial final append hide valid rows. */
23
+ export function parseWatchdogEvents(text) {
24
+ const events = [];
25
+ for (const line of text.split('\n')) {
26
+ if (!line.trim())
27
+ continue;
28
+ try {
29
+ const value = JSON.parse(line);
30
+ if (typeof value.ts !== 'number' ||
31
+ !Number.isFinite(value.ts) ||
32
+ typeof value.kind !== 'string' ||
33
+ !WATCHDOG_EVENT_KINDS.has(value.kind) ||
34
+ typeof value.message !== 'string')
35
+ continue;
36
+ const inspections = Array.isArray(value.inspections)
37
+ ? value.inspections.flatMap((item) => {
38
+ if (item === null || typeof item !== 'object')
39
+ return [];
40
+ const row = item;
41
+ if (typeof row.agentType !== 'string' || typeof row.message !== 'string' || typeof row.reason !== 'string')
42
+ return [];
43
+ return [{
44
+ terminalId: typeof row.terminalId === 'string' ? row.terminalId : undefined,
45
+ agentType: row.agentType,
46
+ message: row.message,
47
+ reason: row.reason,
48
+ stalledForMs: typeof row.stalledForMs === 'number' ? row.stalledForMs : undefined,
49
+ }];
50
+ })
51
+ : undefined;
52
+ events.push({
53
+ ts: value.ts,
54
+ kind: value.kind,
55
+ message: value.message,
56
+ terminalId: typeof value.terminalId === 'string' ? value.terminalId : undefined,
57
+ agentType: typeof value.agentType === 'string' ? value.agentType : undefined,
58
+ reason: typeof value.reason === 'string' ? value.reason : undefined,
59
+ tailLines: Array.isArray(value.tailLines)
60
+ ? value.tailLines.filter((item) => typeof item === 'string')
61
+ : undefined,
62
+ stalledForMs: typeof value.stalledForMs === 'number' ? value.stalledForMs : undefined,
63
+ lastUserMessage: typeof value.lastUserMessage === 'string' ? value.lastUserMessage : undefined,
64
+ lastAssistantMessage: typeof value.lastAssistantMessage === 'string' ? value.lastAssistantMessage : undefined,
65
+ nudgeText: typeof value.nudgeText === 'string' ? value.nudgeText : undefined,
66
+ inspections,
67
+ });
68
+ }
69
+ catch {
70
+ // Interrupted writers can leave one partial row; earlier rows remain useful.
71
+ }
72
+ }
73
+ return events;
74
+ }
75
+ export function readWatchdogEvents(logPath = WATCHDOG_LOG_PATH) {
76
+ if (!fs.existsSync(logPath))
77
+ return [];
78
+ return parseWatchdogEvents(fs.readFileSync(logPath, 'utf8'));
79
+ }
21
80
  /** Serialize one event to a JSONL line (no trailing newline). */
22
81
  export function formatEvent(ev) {
23
82
  return JSON.stringify(ev);
@@ -27,7 +86,25 @@ export function formatEvent(ev) {
27
86
  * writer trims (watchdogLog.ts trimToLast). Large enough to keep a useful
28
87
  * history, small enough to bound the file the UI polls.
29
88
  */
30
- export const WATCHDOG_LOG_MAX_LINES = 2000;
89
+ export const WATCHDOG_LOG_MAX_LINES = 5000;
90
+ export const WATCHDOG_TAIL_MAX_CHARS = 4096;
91
+ /** Keep the newest transcript context without letting it consume the audit window. */
92
+ export function boundTailLines(lines, maxChars = WATCHDOG_TAIL_MAX_CHARS) {
93
+ const kept = [];
94
+ let remaining = maxChars;
95
+ for (let i = lines.length - 1; i >= 0 && remaining > 0; i--) {
96
+ const line = lines[i];
97
+ if (line.length <= remaining) {
98
+ kept.unshift(line);
99
+ remaining -= line.length;
100
+ }
101
+ else {
102
+ kept.unshift(line.slice(-remaining));
103
+ remaining = 0;
104
+ }
105
+ }
106
+ return kept;
107
+ }
31
108
  /** Trim a JSONL body to the last `maxLines` events (same logic as the reader). */
32
109
  export function trimToLast(text, maxLines) {
33
110
  const lines = text.split('\n').filter((l) => l.trim().length > 0);
@@ -58,7 +135,10 @@ export function appendWatchdogEvents(events, opts = {}) {
58
135
  catch {
59
136
  /* first write — no file yet */
60
137
  }
61
- const appended = events.map(formatEvent).join('\n') + '\n';
138
+ const boundedEvents = events.map((event) => event.tailLines === undefined
139
+ ? event
140
+ : { ...event, tailLines: boundTailLines(event.tailLines) });
141
+ const appended = boundedEvents.map(formatEvent).join('\n') + '\n';
62
142
  const body = trimToLast(existing + appended, maxLines);
63
143
  atomicWriteFileSync(logPath, body);
64
144
  });
@@ -137,7 +137,19 @@ export interface SessionOutcome {
137
137
  kind: string;
138
138
  host?: string;
139
139
  cwd?: string;
140
+ project?: string | null;
140
141
  label?: string;
142
+ name?: string;
143
+ topic?: string;
144
+ preview?: string;
145
+ activity?: ActiveSession['activity'];
146
+ status?: ActiveSession['status'];
147
+ startedAtMs?: number;
148
+ lastActivityMs?: number;
149
+ origin?: ActiveSession['origin'];
150
+ routineName?: string;
151
+ machine?: string;
152
+ owner?: string;
141
153
  /** classifyTerminal's verdict for this session. */
142
154
  stall: StallStatus['kind'];
143
155
  /** stalled duration (ms), when stalled. */
@@ -409,7 +409,19 @@ export async function runWatchdogTick(opts = {}) {
409
409
  kind: session.kind,
410
410
  host: session.host,
411
411
  cwd: session.cwd,
412
+ project: session.project,
412
413
  label: session.label,
414
+ name: session.name,
415
+ topic: session.topic,
416
+ preview: session.preview,
417
+ activity: session.activity,
418
+ status: session.status,
419
+ startedAtMs: session.startedAtMs,
420
+ lastActivityMs: session.lastActivityMs,
421
+ origin: session.origin,
422
+ routineName: session.routineName,
423
+ machine: session.machine ?? session.provenance?.host,
424
+ owner: session.owner,
413
425
  policy,
414
426
  stall: 'active',
415
427
  decision: 'skip',
@@ -429,6 +441,7 @@ export async function runWatchdogTick(opts = {}) {
429
441
  continue;
430
442
  }
431
443
  const lastActivityMs = lastActivityFor(session);
444
+ base.lastActivityMs = session.lastActivityMs ?? lastActivityMs;
432
445
  if (lastActivityMs === undefined) {
433
446
  outcomes.push({ ...base, reason: 'no activity timestamp (no transcript / start time)' });
434
447
  continue;
@@ -878,6 +891,13 @@ export async function runWatchdogTick(opts = {}) {
878
891
  ts: nowMs,
879
892
  kind: 'tick',
880
893
  message: `${counts.total} live · ${counts.stalled} stalled · ${counts.nudged} nudged · ${counts.unaddressable} un-addressable`,
894
+ inspections: outcomes.map((outcome) => ({
895
+ terminalId: outcome.sessionId,
896
+ agentType: outcome.kind,
897
+ message: outcome.decision,
898
+ reason: outcome.reason,
899
+ stalledForMs: outcome.stalledForMs,
900
+ })),
881
901
  });
882
902
  appendWatchdogEvents(logEvents, { logPath: opts.logPath });
883
903
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.32",
3
+ "version": "1.22.34",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",