@phnx-labs/agents-cli 1.20.92 → 1.21.0
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.
- package/CHANGELOG.md +385 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/events.js +91 -1
- package/dist/commands/exec.js +13 -0
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +38 -0
- package/dist/commands/projects.js +355 -48
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +198 -7
- package/dist/commands/send.d.ts +14 -12
- package/dist/commands/send.js +105 -35
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +13 -12
- package/dist/commands/view.js +4 -0
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +38 -6
- package/dist/lib/activity.d.ts +15 -0
- package/dist/lib/activity.js +26 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/channels/send.d.ts +83 -0
- package/dist/lib/channels/send.js +112 -0
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events-ingest.d.ts +46 -0
- package/dist/lib/events-ingest.js +182 -0
- package/dist/lib/events.d.ts +17 -5
- package/dist/lib/events.js +59 -56
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +2 -0
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/linear-project-counts.d.ts +129 -0
- package/dist/lib/linear-project-counts.js +206 -0
- package/dist/lib/linear-projects.d.ts +62 -0
- package/dist/lib/linear-projects.js +144 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/menubar/notify-desktop.d.ts +17 -2
- package/dist/lib/menubar/notify-desktop.js +8 -2
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/project-probe.d.ts +75 -0
- package/dist/lib/project-probe.js +160 -0
- package/dist/lib/project-resources.d.ts +8 -0
- package/dist/lib/project-resources.js +31 -3
- package/dist/lib/project-status.d.ts +32 -1
- package/dist/lib/project-status.js +82 -1
- package/dist/lib/projects.d.ts +6 -0
- package/dist/lib/projects.js +12 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routine-notify.d.ts +11 -0
- package/dist/lib/routine-notify.js +22 -0
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/run-notify.js +3 -0
- package/dist/lib/scheduler.js +12 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +53 -10
- package/dist/lib/secrets/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/secrets/list-filter.d.ts +20 -5
- package/dist/lib/secrets/list-filter.js +22 -6
- package/dist/lib/secrets/usage-db.d.ts +106 -0
- package/dist/lib/secrets/usage-db.js +236 -0
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-active.d.ts +5 -1
- package/dist/lib/session/remote-active.js +4 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/sqlite.js +28 -1
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +19 -2
- package/dist/lib/state.js +99 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +22 -5
- package/dist/lib/versions.d.ts +6 -0
- package/dist/lib/versions.js +6 -4
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project workspace probing — the drift signal behind `projects status --fleet`.
|
|
3
|
+
*
|
|
4
|
+
* Projects are natively multi-device: the same definition (home-relative paths)
|
|
5
|
+
* re-roots on every fleet machine, and the question is whether the project's
|
|
6
|
+
* repos are PRESENT on each box, on which branch, how far ahead/behind their
|
|
7
|
+
* upstream, and whether they carry uncommitted changes. This module is the pure
|
|
8
|
+
* local half: given a set of home-relative paths it probes each one with a
|
|
9
|
+
* handful of read-only git calls. Drift is measured against the LAST-FETCHED
|
|
10
|
+
* upstream (`@{upstream}`) — deliberately no `git fetch`, so a probe is fast
|
|
11
|
+
* and offline-safe. The fleet half (`--fleet`) just runs this probe on every
|
|
12
|
+
* peer via the canonical `remote-agents-json` SSH fan-out.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { execFileSync } from 'child_process';
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import { expandLocalHome, toHomeRelative } from './project-root.js';
|
|
19
|
+
/** Per-call git budget. A read-only git call taking >3s is wedged by any
|
|
20
|
+
* definition (NFS stall, index lock) — and the fleet fan-out SIGKILLs the SSH
|
|
21
|
+
* hop at 12s, so a probe must fit inside that budget to avoid a slow peer
|
|
22
|
+
* being misreported as unreachable: 3s × 5 calls leaves headroom even when
|
|
23
|
+
* one repo is genuinely stuck. */
|
|
24
|
+
const GIT_TIMEOUT_MS = 3_000;
|
|
25
|
+
/** One read-only git call against `absPath`; undefined on any failure. */
|
|
26
|
+
function git(absPath, args) {
|
|
27
|
+
try {
|
|
28
|
+
return execFileSync('git', ['-C', absPath, ...args], {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
timeout: GIT_TIMEOUT_MS,
|
|
31
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
32
|
+
}).trim();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Probe one workspace repo. A missing path yields `{present: false}` and no
|
|
40
|
+
* git call is made. On a present repo every signal is best-effort: whatever
|
|
41
|
+
* succeeded is reported, and a repo whose `.git` exists yet every git call
|
|
42
|
+
* failed surfaces as present-with-error rather than silently clean.
|
|
43
|
+
*/
|
|
44
|
+
export function probeRepoWorkspace(absPath) {
|
|
45
|
+
const status = { path: toHomeRelative(absPath), present: false };
|
|
46
|
+
if (!fs.existsSync(path.join(absPath, '.git')))
|
|
47
|
+
return status;
|
|
48
|
+
status.present = true;
|
|
49
|
+
const branch = git(absPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
50
|
+
const upstream = git(absPath, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']);
|
|
51
|
+
// `--left-right --count A...B` prints "<left>\t<right>" — left is
|
|
52
|
+
// upstream-only (we are BEHIND by that much), right is HEAD-only (AHEAD).
|
|
53
|
+
const counts = upstream !== undefined
|
|
54
|
+
? git(absPath, ['rev-list', '--left-right', '--count', '@{upstream}...HEAD'])
|
|
55
|
+
: undefined;
|
|
56
|
+
const dirtyOut = git(absPath, ['status', '--porcelain']);
|
|
57
|
+
const lastCommit = git(absPath, ['log', '-1', '--format=%cI']);
|
|
58
|
+
if (branch === undefined && dirtyOut === undefined && lastCommit === undefined) {
|
|
59
|
+
status.error = '.git exists but git could not read this repo';
|
|
60
|
+
return status;
|
|
61
|
+
}
|
|
62
|
+
if (branch !== undefined)
|
|
63
|
+
status.branch = branch;
|
|
64
|
+
if (upstream !== undefined)
|
|
65
|
+
status.upstream = upstream;
|
|
66
|
+
if (counts !== undefined) {
|
|
67
|
+
const [behind, ahead] = counts.split(/\s+/).map(Number);
|
|
68
|
+
if (Number.isFinite(behind) && Number.isFinite(ahead)) {
|
|
69
|
+
status.behind = behind;
|
|
70
|
+
status.ahead = ahead;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (dirtyOut !== undefined)
|
|
74
|
+
status.dirty = dirtyOut === '' ? 0 : dirtyOut.split('\n').length;
|
|
75
|
+
if (lastCommit !== undefined)
|
|
76
|
+
status.lastCommit = lastCommit;
|
|
77
|
+
return status;
|
|
78
|
+
}
|
|
79
|
+
/** Probe each home-relative path (expanded against the local home), in order. */
|
|
80
|
+
export function probeProjectWorkspaces(paths) {
|
|
81
|
+
return paths.map((p) => probeRepoWorkspace(expandLocalHome(p)));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The home-relative paths to probe for a project definition: its `root` plus
|
|
85
|
+
* each `repos[].path` (the opt-in for additional repos), deduped. Every target
|
|
86
|
+
* is normalized through the same `toHomeRelative(expandLocalHome(...))` the
|
|
87
|
+
* probe echoes, so a hand-edited def (absolute path under home, trailing
|
|
88
|
+
* slash) matches its probe rows exactly — `writeProjectDef` normalizes on
|
|
89
|
+
* write, but defs are hand-editable YAML and never silently drop a row.
|
|
90
|
+
*/
|
|
91
|
+
export function workspaceTargetsForDef(def) {
|
|
92
|
+
const targets = [def.root, ...(def.repos ?? []).map((r) => r.path)]
|
|
93
|
+
.filter((p) => typeof p === 'string' && p.length > 0)
|
|
94
|
+
.map((p) => toHomeRelative(expandLocalHome(p)));
|
|
95
|
+
return [...new Set(targets)];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse a peer's `projects probe --json` stdout, tagging each row with the
|
|
99
|
+
* machine that answered. Defensive against version skew / partial output, the
|
|
100
|
+
* same boundary contract as `parseRemoteActive`: non-JSON or a non-array
|
|
101
|
+
* yields `[]`, and rows without a `path`/`present` core are dropped.
|
|
102
|
+
*/
|
|
103
|
+
export function parseRemoteProbe(stdout, machine) {
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(stdout);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(parsed))
|
|
112
|
+
return [];
|
|
113
|
+
return parsed.flatMap((x) => {
|
|
114
|
+
if (x && typeof x === 'object' && !Array.isArray(x)) {
|
|
115
|
+
const o = x;
|
|
116
|
+
if (typeof o.path === 'string' && typeof o.present === 'boolean') {
|
|
117
|
+
return [{ ...o, host: machine }];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [];
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* One workspace's compact state: `✓ clean · main`, `⚠ 12 dirty · ↑3 ↓1 ·
|
|
125
|
+
* feature/x`, `✗ missing`, or `⚠ error: …`. Pure — chalk styling only.
|
|
126
|
+
*/
|
|
127
|
+
export function formatWorkspaceLine(s) {
|
|
128
|
+
if (!s.present)
|
|
129
|
+
return chalk.red('✗ missing');
|
|
130
|
+
if (s.error)
|
|
131
|
+
return chalk.yellow(`⚠ error: ${s.error}`);
|
|
132
|
+
const parts = [];
|
|
133
|
+
if (s.dirty !== undefined && s.dirty > 0)
|
|
134
|
+
parts.push(`${s.dirty} dirty`);
|
|
135
|
+
const drift = [
|
|
136
|
+
s.ahead !== undefined && s.ahead > 0 ? `↑${s.ahead}` : '',
|
|
137
|
+
s.behind !== undefined && s.behind > 0 ? `↓${s.behind}` : '',
|
|
138
|
+
].filter(Boolean).join(' ');
|
|
139
|
+
if (drift)
|
|
140
|
+
parts.push(drift);
|
|
141
|
+
const head = parts.length > 0 ? chalk.yellow(`⚠ ${parts.join(' · ')}`) : chalk.green('✓ clean');
|
|
142
|
+
return s.branch ? `${head} ${chalk.dim('·')} ${s.branch}` : head;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The fleet view of one project's workspaces: one content line per probed
|
|
146
|
+
* path (host-sorted `host: state` cells joined by ` · `), labelled with the
|
|
147
|
+
* path when a project probes more than one. Pure — the caller adds the
|
|
148
|
+
* `fleet` row label.
|
|
149
|
+
*/
|
|
150
|
+
export function formatFleetWorkspaces(statuses) {
|
|
151
|
+
const paths = [...new Set(statuses.map((s) => s.path))];
|
|
152
|
+
const multi = paths.length > 1;
|
|
153
|
+
return paths.map((p) => {
|
|
154
|
+
const rows = statuses
|
|
155
|
+
.filter((s) => s.path === p)
|
|
156
|
+
.sort((a, b) => a.host.localeCompare(b.host));
|
|
157
|
+
const body = rows.map((r) => `${chalk.cyan(r.host)}: ${formatWorkspaceLine(r)}`).join(chalk.dim(' · '));
|
|
158
|
+
return multi ? `${chalk.dim(`${p} · `)}${body}` : body;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -5,3 +5,11 @@ export interface ProjectResourceSyncResult {
|
|
|
5
5
|
}
|
|
6
6
|
export declare function projectAgentRoot(projectRoot: string, agent: AgentId): string;
|
|
7
7
|
export declare function syncProjectResourcesToAgent(agent: AgentId, version: string, projectAgentsDir: string): ProjectResourceSyncResult;
|
|
8
|
+
/**
|
|
9
|
+
* One human line for the files a project sync left alone because you already
|
|
10
|
+
* wrote them. This is the normal steady state — every sync of a project whose
|
|
11
|
+
* `.claude/commands/` you hand-authored hits it — so it is a single grouped
|
|
12
|
+
* line, not one wrapped warning per file, and it says "yours" rather than the
|
|
13
|
+
* internal "user-owned". Returns null when nothing was skipped.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatKeptProjectResources(skipped: string[]): string | null;
|
|
@@ -127,9 +127,37 @@ function record(kind, name, relPaths, result, manifestPaths) {
|
|
|
127
127
|
manifestPaths.add(toPosixRel(rel));
|
|
128
128
|
}
|
|
129
129
|
function skip(dest, projectRoot, result) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
result.skipped.push(path.relative(projectRoot, dest));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* One human line for the files a project sync left alone because you already
|
|
134
|
+
* wrote them. This is the normal steady state — every sync of a project whose
|
|
135
|
+
* `.claude/commands/` you hand-authored hits it — so it is a single grouped
|
|
136
|
+
* line, not one wrapped warning per file, and it says "yours" rather than the
|
|
137
|
+
* internal "user-owned". Returns null when nothing was skipped.
|
|
138
|
+
*/
|
|
139
|
+
export function formatKeptProjectResources(skipped) {
|
|
140
|
+
if (skipped.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
const rels = [...skipped].sort((a, b) => a.localeCompare(b)).map(toPosixRel);
|
|
143
|
+
if (rels.length === 1)
|
|
144
|
+
return `Kept your existing ${rels[0]}`;
|
|
145
|
+
const byDir = new Map();
|
|
146
|
+
for (const rel of rels) {
|
|
147
|
+
const dir = rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/')) : '.';
|
|
148
|
+
const names = byDir.get(dir) ?? [];
|
|
149
|
+
names.push(rel.slice(rel.lastIndexOf('/') + 1));
|
|
150
|
+
byDir.set(dir, names);
|
|
151
|
+
}
|
|
152
|
+
if (byDir.size === 1) {
|
|
153
|
+
const [dir, names] = [...byDir.entries()][0];
|
|
154
|
+
const PREVIEW = 3;
|
|
155
|
+
const preview = names.slice(0, PREVIEW).join(', ');
|
|
156
|
+
const more = names.length > PREVIEW ? `, +${names.length - PREVIEW} more` : '';
|
|
157
|
+
return `Kept ${rels.length} of your own files in ${dir}: ${preview}${more}`;
|
|
158
|
+
}
|
|
159
|
+
const dirs = [...byDir.entries()].map(([dir, names]) => `${dir} (${names.length})`).join(', ');
|
|
160
|
+
return `Kept ${rels.length} of your own files in ${dirs}`;
|
|
133
161
|
}
|
|
134
162
|
function syncProjectCommands(agent, version, projectAgentsDir, agentRoot, result, manifestPaths) {
|
|
135
163
|
const cfg = AGENTS[agent];
|
|
@@ -13,6 +13,17 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { ActiveSession, ActiveStatus } from './session/active.js';
|
|
15
15
|
import { type ProjectDef } from './projects.js';
|
|
16
|
+
/** One live agent on a project — the WHO behind the byStatus count. */
|
|
17
|
+
export interface ProjectMember {
|
|
18
|
+
/** Harness name (claude / codex / …), from the session's `kind`. */
|
|
19
|
+
agent: string;
|
|
20
|
+
/** Lifecycle status (running / idle / …). */
|
|
21
|
+
status: string;
|
|
22
|
+
/** Tracker ticket the session is tied to, when any. */
|
|
23
|
+
ticket?: string;
|
|
24
|
+
/** Machine the session runs on (provenance host / fleet peer), when known. */
|
|
25
|
+
host?: string;
|
|
26
|
+
}
|
|
16
27
|
/** One project's live session rollup. */
|
|
17
28
|
export interface ProjectSessionRollup {
|
|
18
29
|
name: string;
|
|
@@ -20,6 +31,8 @@ export interface ProjectSessionRollup {
|
|
|
20
31
|
agents: number;
|
|
21
32
|
/** Count per lifecycle status. */
|
|
22
33
|
byStatus: Partial<Record<ActiveStatus, number>>;
|
|
34
|
+
/** Which agents are on the project (one per matched session). */
|
|
35
|
+
members: ProjectMember[];
|
|
23
36
|
/** Summed checklist progress across this project's sessions. */
|
|
24
37
|
plan: {
|
|
25
38
|
done: number;
|
|
@@ -46,7 +59,20 @@ export declare function planPct(plan: {
|
|
|
46
59
|
done: number;
|
|
47
60
|
total: number;
|
|
48
61
|
}): number | undefined;
|
|
49
|
-
/**
|
|
62
|
+
/** Sort members for the card: running first, then idle, then the rest; agent name asc within a state. */
|
|
63
|
+
export declare function sortProjectMembers(members: ProjectMember[]): ProjectMember[];
|
|
64
|
+
/** Cap for the members line before it collapses to `+N more`. */
|
|
65
|
+
export declare const MEMBERS_LINE_LIMIT = 6;
|
|
66
|
+
/**
|
|
67
|
+
* The `agents` line under `live`: one cell per DISTINCT member state —
|
|
68
|
+
* `claude · running · RUSH-2107 @zion` — with identical cells collapsed to a
|
|
69
|
+
* `×N` count (35 same-harness sessions in one state are one fact, not six
|
|
70
|
+
* truncated duplicates), capped at {@link MEMBERS_LINE_LIMIT} cells with a
|
|
71
|
+
* `+N more` tail counting members, not cells. Pure (chalk styling only); the
|
|
72
|
+
* caller adds the label.
|
|
73
|
+
*/
|
|
74
|
+
export declare function formatProjectMembers(members: ProjectMember[], limit?: number): string;
|
|
75
|
+
/** Harvested signals not on the session list: repo-global merged PRs + releases, local artifacts, in a time window. */
|
|
50
76
|
export interface ProjectRemoteSignals {
|
|
51
77
|
windowDays: number;
|
|
52
78
|
/** PRs merged into the primary repo within the window (via `gh`). */
|
|
@@ -55,6 +81,11 @@ export interface ProjectRemoteSignals {
|
|
|
55
81
|
artifacts: number;
|
|
56
82
|
/** Basename of the most recent artifact, when any. */
|
|
57
83
|
lastArtifact?: string;
|
|
84
|
+
/** Latest release of the PRIMARY repo (via `gh release list`), when any. */
|
|
85
|
+
latestRelease?: {
|
|
86
|
+
tag: string;
|
|
87
|
+
publishedAt: string;
|
|
88
|
+
};
|
|
58
89
|
}
|
|
59
90
|
/**
|
|
60
91
|
* Harvest the signals that don't live on the active-session list: recently
|
|
@@ -13,11 +13,21 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { execFile } from 'child_process';
|
|
15
15
|
import { promisify } from 'util';
|
|
16
|
+
import chalk from 'chalk';
|
|
16
17
|
import { projectNameForCwd } from './projects.js';
|
|
17
18
|
import { readRecentActivity } from './activity.js';
|
|
18
19
|
const execFileAsync = promisify(execFile);
|
|
19
20
|
function blank(name) {
|
|
20
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
agents: 0,
|
|
24
|
+
byStatus: {},
|
|
25
|
+
members: [],
|
|
26
|
+
plan: { done: 0, total: 0 },
|
|
27
|
+
openPrs: [],
|
|
28
|
+
tickets: [],
|
|
29
|
+
worktrees: 0,
|
|
30
|
+
};
|
|
21
31
|
}
|
|
22
32
|
/**
|
|
23
33
|
* Roll active sessions up by project. Returns a map keyed by project name,
|
|
@@ -41,6 +51,12 @@ export function rollupSessionsByProject(defs, sessions) {
|
|
|
41
51
|
}
|
|
42
52
|
r.agents++;
|
|
43
53
|
r.byStatus[s.status] = (r.byStatus[s.status] ?? 0) + 1;
|
|
54
|
+
const member = { agent: s.kind, status: s.status };
|
|
55
|
+
if (s.ticket?.id)
|
|
56
|
+
member.ticket = s.ticket.id;
|
|
57
|
+
if (s.machine)
|
|
58
|
+
member.host = s.machine;
|
|
59
|
+
r.members.push(member);
|
|
44
60
|
if (s.todos) {
|
|
45
61
|
r.plan.done += s.todos.done;
|
|
46
62
|
r.plan.total += s.todos.total;
|
|
@@ -67,6 +83,59 @@ export function planPct(plan) {
|
|
|
67
83
|
return undefined;
|
|
68
84
|
return Math.round((plan.done / plan.total) * 100);
|
|
69
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Display order for the members line: the states a human scans for first
|
|
88
|
+
* (running, then idle, then need-input, then queued), everything else after,
|
|
89
|
+
* status name then agent name ascending within a state.
|
|
90
|
+
*/
|
|
91
|
+
const MEMBER_STATUS_RANK = { running: 0, idle: 1, input_required: 2, queued: 3 };
|
|
92
|
+
/** Sort members for the card: running first, then idle, then the rest; agent name asc within a state. */
|
|
93
|
+
export function sortProjectMembers(members) {
|
|
94
|
+
return [...members].sort((a, b) => {
|
|
95
|
+
const ra = MEMBER_STATUS_RANK[a.status] ?? 4;
|
|
96
|
+
const rb = MEMBER_STATUS_RANK[b.status] ?? 4;
|
|
97
|
+
if (ra !== rb)
|
|
98
|
+
return ra - rb;
|
|
99
|
+
if (ra === 4 && a.status !== b.status)
|
|
100
|
+
return a.status.localeCompare(b.status);
|
|
101
|
+
return a.agent.localeCompare(b.agent);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Cap for the members line before it collapses to `+N more`. */
|
|
105
|
+
export const MEMBERS_LINE_LIMIT = 6;
|
|
106
|
+
/**
|
|
107
|
+
* The `agents` line under `live`: one cell per DISTINCT member state —
|
|
108
|
+
* `claude · running · RUSH-2107 @zion` — with identical cells collapsed to a
|
|
109
|
+
* `×N` count (35 same-harness sessions in one state are one fact, not six
|
|
110
|
+
* truncated duplicates), capped at {@link MEMBERS_LINE_LIMIT} cells with a
|
|
111
|
+
* `+N more` tail counting members, not cells. Pure (chalk styling only); the
|
|
112
|
+
* caller adds the label.
|
|
113
|
+
*/
|
|
114
|
+
export function formatProjectMembers(members, limit = MEMBERS_LINE_LIMIT) {
|
|
115
|
+
if (members.length === 0)
|
|
116
|
+
return '';
|
|
117
|
+
// Collapse identical cells — 35 same-harness sessions in the same state are
|
|
118
|
+
// one fact (`claude · running ×16`), not six truncated duplicates.
|
|
119
|
+
const counts = new Map();
|
|
120
|
+
for (const m of sortProjectMembers(members)) {
|
|
121
|
+
const parts = [m.agent, m.status];
|
|
122
|
+
if (m.ticket)
|
|
123
|
+
parts.push(m.ticket);
|
|
124
|
+
const cell = parts.join(' · ') + (m.host ? ` @${m.host}` : '');
|
|
125
|
+
const key = cell.toLowerCase();
|
|
126
|
+
const entry = counts.get(key);
|
|
127
|
+
if (entry)
|
|
128
|
+
entry.n++;
|
|
129
|
+
else
|
|
130
|
+
counts.set(key, { cell, n: 1 });
|
|
131
|
+
}
|
|
132
|
+
const entries = [...counts.values()];
|
|
133
|
+
const shown = entries.slice(0, Math.max(1, limit));
|
|
134
|
+
const shownMembers = shown.reduce((acc, e) => acc + e.n, 0);
|
|
135
|
+
const more = members.length - shownMembers;
|
|
136
|
+
const cells = shown.map(({ cell, n }) => (n > 1 ? `${cell} ×${n}` : cell));
|
|
137
|
+
return cells.join(chalk.dim(' · ')) + (more > 0 ? chalk.dim(` · +${more} more`) : '');
|
|
138
|
+
}
|
|
70
139
|
/**
|
|
71
140
|
* Harvest the signals that don't live on the active-session list: recently
|
|
72
141
|
* merged PRs (from GitHub via `gh`) and artifacts agents produced (from the
|
|
@@ -96,6 +165,18 @@ export async function enrichProjectSignals(def, windowDays, nowMs, opts = {}) {
|
|
|
96
165
|
catch {
|
|
97
166
|
/* gh missing / unauthenticated / repo not found — skip this signal */
|
|
98
167
|
}
|
|
168
|
+
// Latest release of the PRIMARY repo only (repos[] is deliberately not
|
|
169
|
+
// scanned — one release line per card). Same best-effort degradation.
|
|
170
|
+
try {
|
|
171
|
+
const { stdout } = await execFileAsync('gh', ['release', 'list', '-R', def.repo, '-L', '1', '--json', 'tagName,publishedAt'], { timeout: 8000, encoding: 'utf8' });
|
|
172
|
+
const rows = JSON.parse(stdout);
|
|
173
|
+
const first = rows[0];
|
|
174
|
+
if (first?.tagName)
|
|
175
|
+
out.latestRelease = { tag: first.tagName, publishedAt: first.publishedAt ?? '' };
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
/* gh missing / unauthenticated / repo has no releases — skip this signal */
|
|
179
|
+
}
|
|
99
180
|
}
|
|
100
181
|
return out;
|
|
101
182
|
}
|
package/dist/lib/projects.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ export interface ProjectRepo {
|
|
|
24
24
|
slug: string;
|
|
25
25
|
/** Optional path within the repo an agent working this project cares about. */
|
|
26
26
|
subpath?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional home-relative local checkout of this repo. The def's `root` only
|
|
29
|
+
* knows the primary repo on disk; `path` opts an additional repo into
|
|
30
|
+
* workspace probing (`projects status --fleet`).
|
|
31
|
+
*/
|
|
32
|
+
path?: string;
|
|
27
33
|
}
|
|
28
34
|
/**
|
|
29
35
|
* A described context anchor: a subdirectory plus what it is. Agents starting on
|
package/dist/lib/projects.js
CHANGED
|
@@ -86,9 +86,15 @@ export function validateProjectDef(raw, sourceName) {
|
|
|
86
86
|
def.repos = o.repos.flatMap((r) => {
|
|
87
87
|
if (r && typeof r === 'object' && typeof r.slug === 'string') {
|
|
88
88
|
const rr = r;
|
|
89
|
+
// A malformed `path` sinks the whole entry, like any other malformed
|
|
90
|
+
// list row — a half-valid repo must not probe a surprising location.
|
|
91
|
+
if (rr.path !== undefined && typeof rr.path !== 'string')
|
|
92
|
+
return [];
|
|
89
93
|
const repo = { slug: rr.slug };
|
|
90
94
|
if (typeof rr.subpath === 'string')
|
|
91
95
|
repo.subpath = rr.subpath;
|
|
96
|
+
if (typeof rr.path === 'string')
|
|
97
|
+
repo.path = rr.path;
|
|
92
98
|
return [repo];
|
|
93
99
|
}
|
|
94
100
|
return [];
|
|
@@ -192,6 +198,12 @@ export function writeProjectDef(def) {
|
|
|
192
198
|
defaultPath: validated.defaultPath
|
|
193
199
|
? toHomeRelative(expandLocalHome(validated.defaultPath))
|
|
194
200
|
: undefined,
|
|
201
|
+
repos: validated.repos?.map((r) => {
|
|
202
|
+
const repo = { ...r };
|
|
203
|
+
if (r.path)
|
|
204
|
+
repo.path = toHomeRelative(expandLocalHome(r.path));
|
|
205
|
+
return repo;
|
|
206
|
+
}),
|
|
195
207
|
};
|
|
196
208
|
// Drop undefined keys so the YAML stays clean.
|
|
197
209
|
const clean = Object.fromEntries(Object.entries(normalized).filter(([, v]) => v !== undefined));
|
|
@@ -2,7 +2,7 @@ export interface RemoteAgentsJsonOptions<T> {
|
|
|
2
2
|
args: string[];
|
|
3
3
|
noFanoutEnv: string;
|
|
4
4
|
hosts?: string[];
|
|
5
|
-
parse: (stdout: string, machine: string) => T[]
|
|
5
|
+
parse: (stdout: string, machine: string) => T[] | RemoteAgentsJsonParseResult<T>;
|
|
6
6
|
/**
|
|
7
7
|
* Suppress the per-device "unreachable — skipped" stderr line. The skipped
|
|
8
8
|
* names still come back in {@link RemoteAgentsJsonResult.skipped}, so a caller
|
|
@@ -11,12 +11,25 @@ export interface RemoteAgentsJsonOptions<T> {
|
|
|
11
11
|
*/
|
|
12
12
|
quiet?: boolean;
|
|
13
13
|
}
|
|
14
|
+
export interface RemoteAgentsJsonParseResult<T> {
|
|
15
|
+
items: T[];
|
|
16
|
+
valid: boolean;
|
|
17
|
+
}
|
|
14
18
|
export interface RemoteAgentsJsonResult<T> {
|
|
15
19
|
items: T[];
|
|
16
20
|
deviceCount: number;
|
|
17
21
|
/** Devices that were dialed but answered with an error / no CLI / a timeout. */
|
|
18
22
|
skipped: string[];
|
|
23
|
+
/** Devices that exited successfully but returned invalid JSON for this command. */
|
|
24
|
+
parseFailed: string[];
|
|
25
|
+
/** Whether automatic target discovery failed before any peer could be dialed. */
|
|
26
|
+
discoveryFailed: boolean;
|
|
19
27
|
}
|
|
28
|
+
export declare function normalizeRemoteAgentsJsonParse<T>(parsed: T[] | RemoteAgentsJsonParseResult<T>): RemoteAgentsJsonParseResult<T>;
|
|
29
|
+
export declare function parseRemoteAgentsJsonPayload<T>(stdout: string, machine: string, parse: RemoteAgentsJsonOptions<T>['parse']): {
|
|
30
|
+
items: T[];
|
|
31
|
+
parseFailed: boolean;
|
|
32
|
+
};
|
|
20
33
|
/** Build the command one peer runs, with a guard that prevents recursive fan-out. */
|
|
21
34
|
export declare function remoteAgentsJsonCommand(args: string[], noFanoutEnv: string, os?: string): string;
|
|
22
35
|
/** Query explicit hosts, or every registered online peer when hosts is omitted. */
|
|
@@ -15,6 +15,15 @@ import { loadDevices, isControlDevice, isDialableDevice } from './devices/regist
|
|
|
15
15
|
import { remoteShellFor, buildWindowsAgentsCommand } from './hosts/remote-cmd.js';
|
|
16
16
|
import { machineId, normalizeHost } from './machine-id.js';
|
|
17
17
|
const REMOTE_TIMEOUT_MS = 12_000;
|
|
18
|
+
export function normalizeRemoteAgentsJsonParse(parsed) {
|
|
19
|
+
return Array.isArray(parsed) ? { items: parsed, valid: true } : parsed;
|
|
20
|
+
}
|
|
21
|
+
export function parseRemoteAgentsJsonPayload(stdout, machine, parse) {
|
|
22
|
+
const parsed = normalizeRemoteAgentsJsonParse(parse(stdout, machine));
|
|
23
|
+
return parsed.valid
|
|
24
|
+
? { items: parsed.items, parseFailed: false }
|
|
25
|
+
: { items: [], parseFailed: true };
|
|
26
|
+
}
|
|
18
27
|
/** Build the command one peer runs, with a guard that prevents recursive fan-out. */
|
|
19
28
|
export function remoteAgentsJsonCommand(args, noFanoutEnv, os) {
|
|
20
29
|
if (remoteShellFor(os) === 'powershell') {
|
|
@@ -59,7 +68,7 @@ export async function gatherRemoteAgentsJson(options) {
|
|
|
59
68
|
devices = await loadDevices();
|
|
60
69
|
}
|
|
61
70
|
catch {
|
|
62
|
-
return { items: [], deviceCount: 0, skipped: [] };
|
|
71
|
+
return { items: [], deviceCount: 0, skipped: [], parseFailed: [], discoveryFailed: true };
|
|
63
72
|
}
|
|
64
73
|
for (const device of Object.values(devices)) {
|
|
65
74
|
// Live SSH-probe verdict first, cached tailscale snapshot only as a
|
|
@@ -90,6 +99,7 @@ export async function gatherRemoteAgentsJson(options) {
|
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
101
|
const skipped = [];
|
|
102
|
+
const parseFailed = [];
|
|
93
103
|
const results = await Promise.all(targets.map(async (target) => {
|
|
94
104
|
const command = remoteAgentsJsonCommand(options.args, options.noFanoutEnv, target.os);
|
|
95
105
|
const result = await sshCapture(target.target, command);
|
|
@@ -100,7 +110,15 @@ export async function gatherRemoteAgentsJson(options) {
|
|
|
100
110
|
}
|
|
101
111
|
return [];
|
|
102
112
|
}
|
|
103
|
-
|
|
113
|
+
const parsed = parseRemoteAgentsJsonPayload(result.stdout, target.machine, options.parse);
|
|
114
|
+
if (parsed.parseFailed) {
|
|
115
|
+
parseFailed.push(target.name);
|
|
116
|
+
if (!options.quiet) {
|
|
117
|
+
process.stderr.write(chalk.gray(` ${target.name}: unreachable or no agents CLI — skipped\n`));
|
|
118
|
+
}
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
return parsed.items;
|
|
104
122
|
}));
|
|
105
|
-
return { items: results.flat(), deviceCount: targets.length, skipped };
|
|
123
|
+
return { items: results.flat(), deviceCount: targets.length, skipped, parseFailed, discoveryFailed: false };
|
|
106
124
|
}
|
|
@@ -26,6 +26,17 @@ import { type DesktopNotification } from './menubar/notify-desktop.js';
|
|
|
26
26
|
type RoutineKind = 'agent' | 'workflow' | 'command';
|
|
27
27
|
/** Which flavor of routine a config/meta describes — drives the notify threshold. */
|
|
28
28
|
export declare function routineKind(r: Pick<JobConfig, 'agent' | 'workflow' | 'command'>): RoutineKind;
|
|
29
|
+
/**
|
|
30
|
+
* The harness a routine runs on, for the notification's right-hand avatar, or
|
|
31
|
+
* undefined when none owns it. A command routine is deterministic housekeeping
|
|
32
|
+
* with no agent, so it gets no avatar. An agent routine names its own harness.
|
|
33
|
+
* A workflow routine has no `agent` field (the schema omits it — routines.ts
|
|
34
|
+
* `JobConfig.agent` and the validation that rejects setting both), and it runs
|
|
35
|
+
* via `agents run <workflow>`, which delegates to claude under the hood — so its
|
|
36
|
+
* avatar is the Claude mark, matching `effectiveAgent` on the finish path
|
|
37
|
+
* (runner.ts). Start and finish banners therefore show the same avatar.
|
|
38
|
+
*/
|
|
39
|
+
export declare function routineAgent(r: Pick<JobConfig, 'agent' | 'workflow' | 'command'>): string | undefined;
|
|
29
40
|
/** "1m 20s" / "45s" / "2h 3m" from a millisecond duration, or null when unknown. */
|
|
30
41
|
export declare function formatDuration(ms: number | undefined): string | null;
|
|
31
42
|
/**
|
|
@@ -33,6 +33,24 @@ export function routineKind(r) {
|
|
|
33
33
|
return 'workflow';
|
|
34
34
|
return 'agent';
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The harness a routine runs on, for the notification's right-hand avatar, or
|
|
38
|
+
* undefined when none owns it. A command routine is deterministic housekeeping
|
|
39
|
+
* with no agent, so it gets no avatar. An agent routine names its own harness.
|
|
40
|
+
* A workflow routine has no `agent` field (the schema omits it — routines.ts
|
|
41
|
+
* `JobConfig.agent` and the validation that rejects setting both), and it runs
|
|
42
|
+
* via `agents run <workflow>`, which delegates to claude under the hood — so its
|
|
43
|
+
* avatar is the Claude mark, matching `effectiveAgent` on the finish path
|
|
44
|
+
* (runner.ts). Start and finish banners therefore show the same avatar.
|
|
45
|
+
*/
|
|
46
|
+
export function routineAgent(r) {
|
|
47
|
+
const kind = routineKind(r);
|
|
48
|
+
if (kind === 'command')
|
|
49
|
+
return undefined;
|
|
50
|
+
if (kind === 'workflow')
|
|
51
|
+
return 'claude';
|
|
52
|
+
return r.agent?.trim() || undefined;
|
|
53
|
+
}
|
|
36
54
|
/** Human label for the routine body ("agent claude", "workflow deploy", "command"). */
|
|
37
55
|
function routineLabel(r) {
|
|
38
56
|
if (r.command)
|
|
@@ -90,6 +108,7 @@ export function routineStartNotification(config) {
|
|
|
90
108
|
subtitle: config.name,
|
|
91
109
|
body: `Running ${routineLabel(config)}`,
|
|
92
110
|
action: 'routines:list',
|
|
111
|
+
agent: routineAgent(config),
|
|
93
112
|
};
|
|
94
113
|
}
|
|
95
114
|
/**
|
|
@@ -108,6 +127,7 @@ export function routineStartFailedNotification(config, error) {
|
|
|
108
127
|
subtitle: config.name,
|
|
109
128
|
body: `Failed to start: ${error}`,
|
|
110
129
|
action: 'routines:list',
|
|
130
|
+
agent: routineAgent(config),
|
|
111
131
|
};
|
|
112
132
|
}
|
|
113
133
|
/**
|
|
@@ -131,6 +151,7 @@ export function routineFinishNotification(meta, opts = {}) {
|
|
|
131
151
|
subtitle: meta.jobName,
|
|
132
152
|
body: snippet ?? (dur ? `Completed in ${dur}` : 'Completed'),
|
|
133
153
|
action,
|
|
154
|
+
agent: routineAgent(meta),
|
|
134
155
|
};
|
|
135
156
|
}
|
|
136
157
|
// failed | timeout
|
|
@@ -144,6 +165,7 @@ export function routineFinishNotification(meta, opts = {}) {
|
|
|
144
165
|
subtitle: meta.jobName,
|
|
145
166
|
body: reason,
|
|
146
167
|
action,
|
|
168
|
+
agent: routineAgent(meta),
|
|
147
169
|
};
|
|
148
170
|
}
|
|
149
171
|
/** Read a finished run's report text + the best artifact to open on click. */
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -293,6 +293,47 @@ export declare function finalizeRunMeta(meta: RunMeta, status: RunMeta['status']
|
|
|
293
293
|
* catchup/overdue, manual run) gates on this.
|
|
294
294
|
*/
|
|
295
295
|
export declare function jobRunsOnThisDevice(config: Pick<JobConfig, 'devices'>): boolean;
|
|
296
|
+
/**
|
|
297
|
+
* The ONE device that owns a routine — the single daemon allowed to fire it.
|
|
298
|
+
*
|
|
299
|
+
* `devices` is an allowlist, and every listed device used to fire
|
|
300
|
+
* independently, so a routine pinned to two boxes ran **twice** per schedule:
|
|
301
|
+
* two full agent sessions doing identical work, burning double the quota. On
|
|
302
|
+
* this fleet seven routines were in that state, e.g. `security-sweep` running
|
|
303
|
+
* at 15:30:02 on one box and 15:30:03 on the other, both completing.
|
|
304
|
+
*
|
|
305
|
+
* Ownership is a pure function of the config — the first entry in normalized
|
|
306
|
+
* sort order — so every daemon independently reaches the same answer with no
|
|
307
|
+
* lease, no cross-device coordination, and no split brain when the fleet
|
|
308
|
+
* partitions. A multi-entry pin is a misconfiguration
|
|
309
|
+
* ({@link hasAmbiguousDevicePin}); this keeps such a routine running exactly
|
|
310
|
+
* once instead of silently dropping it, while `validateJob` refuses to create
|
|
311
|
+
* a new one and `agents doctor` surfaces the existing ones.
|
|
312
|
+
*
|
|
313
|
+
* Returns null when the routine is unrestricted (empty or omitted `devices`).
|
|
314
|
+
*/
|
|
315
|
+
export declare function routineOwnerDevice(config: Pick<JobConfig, 'devices'>): string | null;
|
|
316
|
+
/**
|
|
317
|
+
* Does this routine name more than one distinct device? Such a pin used to mean
|
|
318
|
+
* "fire on each of them"; it now means "fire only on the first", which is
|
|
319
|
+
* almost certainly not what the author intended either way — so it is reported
|
|
320
|
+
* as a misconfiguration rather than silently reinterpreted.
|
|
321
|
+
*/
|
|
322
|
+
export declare function hasAmbiguousDevicePin(config: Pick<JobConfig, 'devices'>): boolean;
|
|
323
|
+
/** One routine whose `devices` names more than one machine, with its resolved owner. */
|
|
324
|
+
export interface AmbiguousDevicePin {
|
|
325
|
+
name: string;
|
|
326
|
+
devices: string[];
|
|
327
|
+
/** The device that now fires it — the rest are inert. */
|
|
328
|
+
owner: string;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Every routine carrying a multi-device pin. Surfaced by `agents doctor` and
|
|
332
|
+
* `agents routines list` so an existing misconfiguration is visible rather than
|
|
333
|
+
* silently reinterpreted: before ownership became singular each of these fired
|
|
334
|
+
* once per listed device, doubling the work and the agent spend.
|
|
335
|
+
*/
|
|
336
|
+
export declare function findAmbiguousDevicePins(cwd?: string): AmbiguousDevicePin[];
|
|
296
337
|
/**
|
|
297
338
|
* Resolve the effective host strategy for a job.
|
|
298
339
|
* Bare `host:` without an explicit strategy implies `host` (back-compat with
|
|
@@ -487,6 +528,18 @@ export declare function discoverJobsFromRepo(repoPath: string): Array<{
|
|
|
487
528
|
export declare function jobExists(name: string): boolean;
|
|
488
529
|
/** Get the filesystem path of a job's YAML config file, or null if not found. */
|
|
489
530
|
export declare function getJobPath(name: string): string | null;
|
|
531
|
+
/**
|
|
532
|
+
* Resolve a routine's YAML across EVERY layer `listJobs`/`readJob` read — user
|
|
533
|
+
* then system — not just the user dir.
|
|
534
|
+
*
|
|
535
|
+
* `getJobPath` is user-layer only because its callers write there. Read paths
|
|
536
|
+
* that ask "when did this routine come to exist" need the system layer too:
|
|
537
|
+
* a built-in shipped in the system repo has no user-layer file and no
|
|
538
|
+
* `createdAt`, so a user-layer-only lookup returns null, the overdue floor is
|
|
539
|
+
* skipped, and the routine reads as instantly overdue on first daemon start —
|
|
540
|
+
* exactly the case the floor exists to prevent.
|
|
541
|
+
*/
|
|
542
|
+
export declare function resolveJobFilePath(name: string): string | null;
|
|
490
543
|
/**
|
|
491
544
|
* Parse an "at" time string into a one-shot cron expression.
|
|
492
545
|
* Supports formats like:
|