@phnx-labs/agents-cli 1.22.30 → 1.22.32

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 (83) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +13 -6
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/daemon.js +52 -12
  5. package/dist/commands/doctor.d.ts +19 -0
  6. package/dist/commands/doctor.js +119 -17
  7. package/dist/commands/focus.d.ts +4 -1
  8. package/dist/commands/focus.js +19 -4
  9. package/dist/commands/routines.js +164 -36
  10. package/dist/commands/secrets.d.ts +20 -0
  11. package/dist/commands/secrets.js +59 -93
  12. package/dist/commands/{sessions-favorite.d.ts → sessions-bookmark.d.ts} +7 -7
  13. package/dist/commands/{sessions-favorite.js → sessions-bookmark.js} +38 -38
  14. package/dist/commands/sessions-browser.d.ts +10 -8
  15. package/dist/commands/sessions-browser.js +61 -32
  16. package/dist/commands/sessions-stats.js +1 -1
  17. package/dist/commands/sessions.d.ts +11 -9
  18. package/dist/commands/sessions.js +114 -65
  19. package/dist/commands/update.d.ts +2 -0
  20. package/dist/commands/update.js +148 -0
  21. package/dist/index.js +4 -2
  22. package/dist/lib/catchup.js +4 -1
  23. package/dist/lib/daemon.d.ts +17 -0
  24. package/dist/lib/daemon.js +69 -3
  25. package/dist/lib/devices/doctor-findings.d.ts +7 -2
  26. package/dist/lib/devices/doctor-findings.js +53 -2
  27. package/dist/lib/devices/doctor-overview-cache.d.ts +7 -0
  28. package/dist/lib/devices/doctor-overview-cache.js +15 -0
  29. package/dist/lib/devices/fleet-divergence.d.ts +11 -0
  30. package/dist/lib/devices/fleet-divergence.js +6 -0
  31. package/dist/lib/devices/fleet-inventory.js +16 -2
  32. package/dist/lib/drift.d.ts +6 -1
  33. package/dist/lib/drift.js +9 -0
  34. package/dist/lib/hooks/cache.js +20 -1
  35. package/dist/lib/hooks.d.ts +91 -1
  36. package/dist/lib/hooks.js +289 -3
  37. package/dist/lib/hosts/passthrough.js +3 -0
  38. package/dist/lib/installations/index.d.ts +14 -0
  39. package/dist/lib/installations/index.js +14 -0
  40. package/dist/lib/installations/resolve.d.ts +43 -0
  41. package/dist/lib/installations/resolve.js +93 -0
  42. package/dist/lib/installations/store.d.ts +56 -0
  43. package/dist/lib/installations/store.js +196 -0
  44. package/dist/lib/installations/strategies.d.ts +73 -0
  45. package/dist/lib/installations/strategies.js +293 -0
  46. package/dist/lib/installations/types.d.ts +78 -0
  47. package/dist/lib/installations/types.js +8 -0
  48. package/dist/lib/installations/update.d.ts +40 -0
  49. package/dist/lib/installations/update.js +131 -0
  50. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  51. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  52. package/dist/lib/migrate.d.ts +27 -0
  53. package/dist/lib/migrate.js +118 -2
  54. package/dist/lib/picker.d.ts +6 -3
  55. package/dist/lib/picker.js +7 -2
  56. package/dist/lib/routine-context.d.ts +144 -0
  57. package/dist/lib/routine-context.js +268 -0
  58. package/dist/lib/routine-readiness.d.ts +47 -0
  59. package/dist/lib/routine-readiness.js +239 -0
  60. package/dist/lib/routines.d.ts +97 -1
  61. package/dist/lib/routines.js +107 -1
  62. package/dist/lib/runner.d.ts +18 -4
  63. package/dist/lib/runner.js +291 -98
  64. package/dist/lib/scheduler.d.ts +7 -1
  65. package/dist/lib/scheduler.js +5 -2
  66. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  67. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  68. package/dist/lib/secrets/agent.d.ts +0 -1
  69. package/dist/lib/secrets/agent.js +0 -4
  70. package/dist/lib/secrets/session-store.d.ts +0 -4
  71. package/dist/lib/secrets/session-store.js +0 -5
  72. package/dist/lib/self-heal/checks/hook-runtime.d.ts +2 -0
  73. package/dist/lib/self-heal/checks/hook-runtime.js +16 -0
  74. package/dist/lib/self-heal/registry.js +5 -2
  75. package/dist/lib/self-heal/types.d.ts +1 -1
  76. package/dist/lib/session/{favorites.d.ts → bookmarks.d.ts} +14 -14
  77. package/dist/lib/session/{favorites.js → bookmarks.js} +22 -22
  78. package/dist/lib/session/state.js +4 -1
  79. package/dist/lib/startup/command-registry.d.ts +1 -0
  80. package/dist/lib/startup/command-registry.js +2 -0
  81. package/dist/lib/versions.d.ts +24 -0
  82. package/dist/lib/versions.js +49 -16
  83. package/package.json +2 -2
@@ -6,6 +6,7 @@
6
6
  * run metadata persistence, prompt variable expansion, and one-shot "at" time
7
7
  * scheduling.
8
8
  */
9
+ import { type ResolvedExecutionContext, type ProjectResolution, type PlacementMode, type RoutineKind, type ContextFsProbe } from './routine-context.js';
9
10
  import type { AgentId } from './types.js';
10
11
  import type { LoopConfig } from './loop.js';
11
12
  /** Tool/site/directory allow-list for sandboxed job execution. */
@@ -114,6 +115,32 @@ export interface JobConfig {
114
115
  prompt: string;
115
116
  timezone?: string;
116
117
  repo?: string;
118
+ /**
119
+ * Singular execution anchor: the named project (`agents projects`) whose base
120
+ * directory the routine's run lands in. Optional. Metadata-only `projects[]`
121
+ * (below) is NEVER used for execution — this field is. Resolution happens on
122
+ * the execution TARGET (`resolveRoutineExecutionContext`, routine-context.ts),
123
+ * never from the daemon's own cwd: a project with a usable `defaultPath`/`root`
124
+ * gives the base directory; a rootless Linear-imported project gives no base,
125
+ * so a bare relative `cwd` then anchors at the target user's `$HOME`.
126
+ *
127
+ * CLI flag is `--project-anchor` (not `--project`, which is the repeatable
128
+ * grouping-metadata flag that writes `projects[]`). The YAML key is the shorter
129
+ * singular `project` because it is unambiguous there.
130
+ */
131
+ project?: string;
132
+ /**
133
+ * Portable execution directory for the routine's run. Optional. A relative
134
+ * value resolves under the `project` base when that base is usable, otherwise
135
+ * under the execution target's `$HOME` (so a Linear-imported rootless project
136
+ * can still name `cwd: src/github.com/acme/app`). A `~/`-anchored value is the
137
+ * target's home-relative path; an absolute path under the target home is
138
+ * normalized to the portable `~/…` form on save. An absolute path outside the
139
+ * home is only allowed for local-pinned routines — host/fleet/cloud placement
140
+ * pauses it as non-portable. Supersedes the legacy `remoteCwd`, which the
141
+ * one-shot migration folds into this field.
142
+ */
143
+ cwd?: string;
117
144
  /**
118
145
  * Fleet allowlist — restrict this routine to specific devices. When omitted
119
146
  * or empty, the routine is unrestricted and fires on every device running the
@@ -308,6 +335,24 @@ export declare function projectGroupOrder(group: ProjectGroup): number;
308
335
  * - `"Unknown projects"` — when any entry is no longer a defined project (stale).
309
336
  */
310
337
  export declare function computeProjectGroup(projects: string[] | undefined, knownProjectNames: Set<string>): string;
338
+ /** A real-filesystem {@link ContextFsProbe} for readiness checks on this machine. */
339
+ export declare function realFsProbe(): ContextFsProbe;
340
+ /** Classify a routine by its body kind — governs the execution-context fallback rules. */
341
+ export declare function jobRoutineKind(config: Pick<JobConfig, 'agent' | 'workflow' | 'command'>): RoutineKind;
342
+ /**
343
+ * Resolve a routine's execution context (working directory + structural/fs
344
+ * readiness) by bridging its `project`/`cwd` fields into the pure
345
+ * {@link resolveRoutineExecutionContext} resolver. Local placement resolves
346
+ * against this machine's `$HOME` with a real filesystem probe; a caller may
347
+ * inject a different target home / probe (e.g. `null` to defer existence for a
348
+ * remote target).
349
+ */
350
+ export declare function resolveJobExecutionContext(config: Pick<JobConfig, 'name' | 'project' | 'cwd' | 'agent' | 'workflow' | 'command'>, opts?: {
351
+ targetHome?: string;
352
+ mode?: PlacementMode;
353
+ probe?: ContextFsProbe | null;
354
+ projectResolution?: ProjectResolution;
355
+ }): ResolvedExecutionContext;
311
356
  /** Metadata for a single job execution, persisted as JSON in the run directory. */
312
357
  export interface RunMeta {
313
358
  jobName: string;
@@ -337,8 +382,41 @@ export interface RunMeta {
337
382
  * due). Without it a miss leaves no trace at all and the listing keeps
338
383
  * showing the previous run's status as if it were current. Written by
339
384
  * `claimMissedFire` (catchup.ts), never by the runner.
385
+ *
386
+ * `blocked` and `skipped` are pre-execution terminals that leave a visible
387
+ * record even though no agent process ran (the plan's history contract):
388
+ * - `blocked` — a fire-time readiness rejection (bad context, dead auth,
389
+ * untrusted workspace). No agent process was spawned. Distinct from `failed`,
390
+ * which means a process started and failed.
391
+ * - `skipped` — the attempt lost a claim (`skipReason`): a duplicate schedule
392
+ * slot, an already-active run it would overlap, or a wrong device owner.
393
+ */
394
+ status: 'running' | 'completed' | 'failed' | 'timeout' | 'missed' | 'blocked' | 'skipped';
395
+ /**
396
+ * How this attempt was triggered. Answers "why did this run exist" for a
397
+ * record that may have no transcript (a blocked/skipped attempt).
398
+ */
399
+ triggerKind?: 'schedule' | 'catchup' | 'manual' | 'webhook' | 'event';
400
+ /**
401
+ * The scheduler's intended UTC fire time (ISO), for a `schedule`/`catchup`
402
+ * attempt. The atomic single-fire claim keys on (routine, scheduledFor): a
403
+ * duplicate cron delivery for the same slot resolves to this same run rather
404
+ * than launching a second time.
340
405
  */
341
- status: 'running' | 'completed' | 'failed' | 'timeout' | 'missed';
406
+ scheduledFor?: string;
407
+ /** Resolved execution context (routine-context.ts), recorded before preflight. */
408
+ project?: string;
409
+ requestedCwd?: string;
410
+ resolvedCwd?: string;
411
+ readiness?: {
412
+ code: string;
413
+ message: string;
414
+ repair?: string;
415
+ };
416
+ /** Why a `skipped` attempt launched nothing. */
417
+ skipReason?: 'duplicate_slot' | 'active_run' | 'wrong_owner';
418
+ /** The run this attempt deferred to (the winning duplicate slot / active run). */
419
+ activeRunId?: string;
342
420
  startedAt: string;
343
421
  completedAt: string | null;
344
422
  exitCode: number | null;
@@ -629,6 +707,24 @@ export declare function readRunMeta(jobName: string, runId: string): RunMeta | n
629
707
  export declare function getJobRunsDir(jobName: string): string;
630
708
  /** Get the filesystem path for a specific run's directory. */
631
709
  export declare function getRunDir(jobName: string, runId: string): string;
710
+ /**
711
+ * The run id a scheduled fire is recorded under — derived from its intended UTC
712
+ * fire time so the SAME slot always maps to the SAME run directory. This is what
713
+ * makes the single-fire claim meaningful: a duplicate cron delivery for one slot
714
+ * computes the same id and loses the atomic `mkdir` claim. Shares the derivation
715
+ * with `missedRunId` (catchup.ts) so a missed-then-caught-up fire and a live fire
716
+ * for the same UTC slot are one record.
717
+ */
718
+ export declare function slotRunId(scheduledFor: Date | string): string;
719
+ /**
720
+ * Atomically CLAIM a run directory. Returns true on a successful claim, false
721
+ * when the directory already exists (another caller — even in a separate process
722
+ * — owns this (routine, slot) pair). The non-recursive `mkdir` is a single
723
+ * filesystem test-and-set on every POSIX filesystem, the same primitive
724
+ * `claimMissedFire` relies on; it holds across processes where an in-process flag
725
+ * or a released lock cannot.
726
+ */
727
+ export declare function claimRunSlot(jobName: string, runId: string): boolean;
632
728
  /** Discover routine YAML files in a repository's routines/ directory. */
633
729
  export declare function discoverJobsFromRepo(repoPath: string): Array<{
634
730
  name: string;
@@ -11,8 +11,10 @@ import * as path from 'path';
11
11
  import * as yaml from 'yaml';
12
12
  import { Cron } from 'croner';
13
13
  import { getRoutinesDir, getSystemRoutinesDir, getRunsDir, ensureAgentsDir, getProjectRoutinesDir } from './state.js';
14
+ import * as os from 'os';
14
15
  import { safeJoin, isSafeSegmentName } from './paths.js';
15
- import { isSafeProjectName } from './projects.js';
16
+ import { isSafeProjectName, loadProjectDef, projectBasePath } from './projects.js';
17
+ import { resolveRoutineExecutionContext, } from './routine-context.js';
16
18
  import { atomicWriteFileSync } from './fs-atomic.js';
17
19
  import { ALL_AGENT_IDS, ROUTINE_AGENT_IDS } from './agents.js';
18
20
  import { machineId, normalizeHost } from './machine-id.js';
@@ -142,6 +144,65 @@ export function projectGroupOrder(group) {
142
144
  export function computeProjectGroup(projects, knownProjectNames) {
143
145
  return projectGroupTitle(computeProjectGroupKind(projects, knownProjectNames));
144
146
  }
147
+ /** A real-filesystem {@link ContextFsProbe} for readiness checks on this machine. */
148
+ export function realFsProbe() {
149
+ return {
150
+ exists: (p) => fs.existsSync(p),
151
+ isDirectory: (p) => { try {
152
+ return fs.statSync(p).isDirectory();
153
+ }
154
+ catch {
155
+ return false;
156
+ } },
157
+ isWritable: (p) => { try {
158
+ fs.accessSync(p, fs.constants.W_OK);
159
+ return true;
160
+ }
161
+ catch {
162
+ return false;
163
+ } },
164
+ };
165
+ }
166
+ /** Classify a routine by its body kind — governs the execution-context fallback rules. */
167
+ export function jobRoutineKind(config) {
168
+ if (config.command)
169
+ return 'command';
170
+ if (config.workflow)
171
+ return 'workflow';
172
+ return 'agent';
173
+ }
174
+ /**
175
+ * Resolve a routine's execution context (working directory + structural/fs
176
+ * readiness) by bridging its `project`/`cwd` fields into the pure
177
+ * {@link resolveRoutineExecutionContext} resolver. Local placement resolves
178
+ * against this machine's `$HOME` with a real filesystem probe; a caller may
179
+ * inject a different target home / probe (e.g. `null` to defer existence for a
180
+ * remote target).
181
+ */
182
+ export function resolveJobExecutionContext(config, opts = {}) {
183
+ const targetHome = opts.targetHome ?? os.homedir();
184
+ const mode = opts.mode ?? 'local';
185
+ const probe = opts.probe === null
186
+ ? undefined
187
+ : (opts.probe ?? (mode === 'local' ? realFsProbe() : undefined));
188
+ let projectResolution = opts.projectResolution;
189
+ if (config.project !== undefined && projectResolution === undefined) {
190
+ const def = loadProjectDef(config.project);
191
+ projectResolution = def
192
+ ? { defined: true, base: projectBasePath(def, true) } // portable (~/) base form
193
+ : { defined: false };
194
+ }
195
+ return resolveRoutineExecutionContext({
196
+ name: config.name,
197
+ project: config.project,
198
+ cwd: config.cwd,
199
+ kind: jobRoutineKind(config),
200
+ mode,
201
+ targetHome,
202
+ projectResolution,
203
+ probe,
204
+ });
205
+ }
145
206
  /**
146
207
  * Finalize a run record with a terminal status, computing `duration` from
147
208
  * `startedAt` and the completion timestamp. Keeps failure-reason population
@@ -699,6 +760,18 @@ export function validateJob(config) {
699
760
  if (config.remoteCwd !== undefined && strategy !== 'host' && strategy !== 'fleet') {
700
761
  errors.push('remoteCwd only applies to host/fleet-placed routines — set hostStrategy: host|fleet, or drop it');
701
762
  }
763
+ if (config.project !== undefined && (typeof config.project !== 'string' || config.project.trim() === '')) {
764
+ errors.push('project (the singular execution anchor) must be a non-empty project name');
765
+ }
766
+ if (config.cwd !== undefined && (typeof config.cwd !== 'string' || config.cwd.trim() === '')) {
767
+ errors.push('cwd (the portable execution directory) must be a non-empty path string');
768
+ }
769
+ // `remoteCwd` is the legacy host-placement path; `cwd` is its canonical
770
+ // replacement. The two split path semantics, so they must never coexist — the
771
+ // one-shot migration folds remoteCwd into cwd, and a conflicting pair pauses.
772
+ if (config.cwd !== undefined && config.remoteCwd !== undefined) {
773
+ errors.push('cwd and remoteCwd both set — remoteCwd is the legacy form of cwd; keep only cwd');
774
+ }
702
775
  if (config.source !== undefined) {
703
776
  if (!config.source || typeof config.source !== 'object') {
704
777
  errors.push('source must be an object');
@@ -1168,6 +1241,39 @@ export function getJobRunsDir(jobName) {
1168
1241
  export function getRunDir(jobName, runId) {
1169
1242
  return path.join(getJobRunsDir(jobName), runId);
1170
1243
  }
1244
+ /**
1245
+ * The run id a scheduled fire is recorded under — derived from its intended UTC
1246
+ * fire time so the SAME slot always maps to the SAME run directory. This is what
1247
+ * makes the single-fire claim meaningful: a duplicate cron delivery for one slot
1248
+ * computes the same id and loses the atomic `mkdir` claim. Shares the derivation
1249
+ * with `missedRunId` (catchup.ts) so a missed-then-caught-up fire and a live fire
1250
+ * for the same UTC slot are one record.
1251
+ */
1252
+ export function slotRunId(scheduledFor) {
1253
+ const iso = typeof scheduledFor === 'string' ? scheduledFor : scheduledFor.toISOString();
1254
+ return iso.replace(/[:.]/g, '-');
1255
+ }
1256
+ /**
1257
+ * Atomically CLAIM a run directory. Returns true on a successful claim, false
1258
+ * when the directory already exists (another caller — even in a separate process
1259
+ * — owns this (routine, slot) pair). The non-recursive `mkdir` is a single
1260
+ * filesystem test-and-set on every POSIX filesystem, the same primitive
1261
+ * `claimMissedFire` relies on; it holds across processes where an in-process flag
1262
+ * or a released lock cannot.
1263
+ */
1264
+ export function claimRunSlot(jobName, runId) {
1265
+ const runDir = getRunDir(jobName, runId);
1266
+ fs.mkdirSync(path.dirname(runDir), { recursive: true });
1267
+ try {
1268
+ fs.mkdirSync(runDir); // non-recursive: throws EEXIST if already claimed
1269
+ return true;
1270
+ }
1271
+ catch (err) {
1272
+ if (err.code === 'EEXIST')
1273
+ return false;
1274
+ throw err;
1275
+ }
1276
+ }
1171
1277
  /** Discover routine YAML files in a repository's routines/ directory. */
1172
1278
  export function discoverJobsFromRepo(repoPath) {
1173
1279
  const jobsPath = path.join(repoPath, 'routines');
@@ -26,8 +26,22 @@ export interface RunResult {
26
26
  export declare class RoutineAlreadyRunningError extends Error {
27
27
  constructor(jobName: string, runId: string);
28
28
  }
29
- /** Stable working directory for routine children, independent of the daemon's launch cwd. */
30
- export declare function routineSpawnCwd(config: Pick<JobConfig, 'repo'>, configuredRoot?: string | undefined): string;
29
+ /** How a routine attempt was triggered, plus the schedule slot it belongs to. */
30
+ export interface RoutineTrigger {
31
+ kind: NonNullable<RunMeta['triggerKind']>;
32
+ /** UTC fire time for a schedule/catchup attempt; keys the single-fire slot claim. */
33
+ scheduledFor?: Date | string;
34
+ }
35
+ /**
36
+ * Working directory for a routine's LOCAL child, resolved from its explicit
37
+ * `project`/`cwd` execution anchor via {@link resolveJobExecutionContext} — never
38
+ * inferred from `repo` (which is external repository identity only) and never the
39
+ * daemon's launch cwd. A command routine with neither field lands in `$HOME`
40
+ * (housekeeping); an unresolved/blocked agent context also falls back to `$HOME`
41
+ * so a caller that reaches this (past the readiness gate) has a valid directory,
42
+ * but the gate should have paused such a routine before it ever spawned.
43
+ */
44
+ export declare function routineSpawnCwd(config: Pick<JobConfig, 'name' | 'project' | 'cwd' | 'agent' | 'workflow' | 'command'>): string;
31
45
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
32
46
  export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string): string[];
33
47
  /**
@@ -86,7 +100,7 @@ export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow
86
100
  * with `agents run`.
87
101
  */
88
102
  export declare function buildRoutineSpawnEnv(baseEnv: Record<string, string>, agent: AgentId, version: string | undefined, timezone?: string, overlayHome?: string): Record<string, string>;
89
- export declare function executeJob(config: JobConfig, deps?: LoopDeps): Promise<RunResult>;
103
+ export declare function executeJob(config: JobConfig, deps?: LoopDeps, trigger?: RoutineTrigger): Promise<RunResult>;
90
104
  /**
91
105
  * Optional lifecycle callbacks for a detached routine run. The daemon passes an
92
106
  * `onFinish` that fires the branded finish/output notification (RUSH-2030) — it
@@ -99,7 +113,7 @@ export interface RoutineHooks {
99
113
  onFinish?: (meta: RunMeta) => void;
100
114
  }
101
115
  /** Spawn a job as a detached process and return immediately with run metadata. */
102
- export declare function executeJobDetached(config: JobConfig, hooks?: RoutineHooks): Promise<RunMeta>;
116
+ export declare function executeJobDetached(config: JobConfig, hooks?: RoutineHooks, trigger?: RoutineTrigger): Promise<RunMeta>;
103
117
  /** Extract the final assistant message from a stream-JSON log file as a markdown report. */
104
118
  export declare function extractReport(stdoutPath: string, agentType: AgentId): string | null;
105
119
  /** Derive the final status of a detached run by reading the agent's stream-json