@phnx-labs/agents-cli 1.20.90 → 1.20.92
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 +276 -0
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +72 -6
- package/dist/commands/activity.js +198 -49
- package/dist/commands/beta.js +1 -0
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/exec.d.ts +14 -0
- package/dist/commands/exec.js +144 -14
- package/dist/commands/feed.js +77 -4
- package/dist/commands/hooks.js +22 -6
- package/dist/commands/perf.d.ts +14 -0
- package/dist/commands/perf.js +221 -0
- package/dist/commands/projects.d.ts +12 -0
- package/dist/commands/projects.js +358 -0
- package/dist/commands/routines.js +30 -24
- package/dist/commands/secrets.d.ts +43 -4
- package/dist/commands/secrets.js +217 -32
- package/dist/commands/send.d.ts +5 -1
- package/dist/commands/send.js +1 -1
- package/dist/commands/sessions-picker.d.ts +15 -0
- package/dist/commands/sessions-picker.js +107 -13
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +9 -1
- package/dist/commands/sessions.d.ts +10 -5
- package/dist/commands/sessions.js +65 -27
- package/dist/index.js +19 -3
- package/dist/lib/activity.d.ts +80 -13
- package/dist/lib/activity.js +418 -74
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/catchup.d.ts +105 -0
- package/dist/lib/catchup.js +160 -0
- package/dist/lib/channels/providers/desktop.d.ts +49 -0
- package/dist/lib/channels/providers/desktop.js +132 -0
- package/dist/lib/channels/providers/index.js +2 -0
- package/dist/lib/daemon.js +74 -13
- package/dist/lib/devices/registry.d.ts +14 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/events.d.ts +12 -0
- package/dist/lib/events.js +122 -9
- package/dist/lib/exec.js +10 -0
- package/dist/lib/feed-broadcast.d.ts +47 -0
- package/dist/lib/feed-broadcast.js +65 -1
- package/dist/lib/feed-post.d.ts +10 -0
- package/dist/lib/feed-post.js +9 -3
- package/dist/lib/feed.d.ts +47 -1
- package/dist/lib/feed.js +38 -0
- package/dist/lib/hooks/cache.d.ts +2 -0
- package/dist/lib/hooks/cache.js +24 -4
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +14 -4
- package/dist/lib/menubar/install-menubar.js +20 -6
- package/dist/lib/overdue.d.ts +14 -0
- package/dist/lib/overdue.js +37 -1
- package/dist/lib/perf/db.d.ts +25 -0
- package/dist/lib/perf/db.js +290 -0
- package/dist/lib/perf/spool.d.ts +18 -0
- package/dist/lib/perf/spool.js +79 -0
- package/dist/lib/perf/types.d.ts +45 -0
- package/dist/lib/perf/types.js +2 -0
- package/dist/lib/project-key.d.ts +44 -0
- package/dist/lib/project-key.js +79 -0
- package/dist/lib/project-root.js +16 -0
- package/dist/lib/project-status.d.ts +69 -0
- package/dist/lib/project-status.js +101 -0
- package/dist/lib/projects.d.ts +138 -0
- package/dist/lib/projects.js +301 -0
- package/dist/lib/remote-agents-json.d.ts +9 -0
- package/dist/lib/remote-agents-json.js +11 -5
- package/dist/lib/routines-project.js +6 -0
- package/dist/lib/routines.d.ts +30 -1
- package/dist/lib/routines.js +11 -0
- 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/list-filter.d.ts +94 -0
- package/dist/lib/secrets/list-filter.js +245 -0
- package/dist/lib/session/bash-command.d.ts +53 -0
- package/dist/lib/session/bash-command.js +364 -0
- package/dist/lib/session/digest.d.ts +13 -0
- package/dist/lib/session/digest.js +48 -1
- package/dist/lib/session/discover.d.ts +1 -2
- package/dist/lib/session/discover.js +7 -24
- package/dist/lib/session/highlights.d.ts +82 -0
- package/dist/lib/session/highlights.js +251 -0
- package/dist/lib/session/parse.js +23 -1
- package/dist/lib/session/relative-time.d.ts +37 -0
- package/dist/lib/session/relative-time.js +96 -8
- package/dist/lib/session/remote-list.js +5 -2
- package/dist/lib/session/render.d.ts +9 -9
- package/dist/lib/session/render.js +112 -73
- package/dist/lib/session/types.d.ts +4 -1
- package/dist/lib/ssh-exec.d.ts +6 -0
- package/dist/lib/ssh-exec.js +10 -1
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +11 -0
- package/dist/lib/state.js +16 -0
- package/dist/lib/terminal/backends/index.d.ts +10 -2
- package/dist/lib/terminal/backends/index.js +14 -2
- package/dist/lib/terminal/backends/terminal-app.d.ts +13 -0
- package/dist/lib/terminal/backends/terminal-app.js +73 -0
- package/dist/lib/terminal/index.d.ts +2 -1
- package/dist/lib/terminal/index.js +2 -1
- package/dist/lib/terminal/preferred.d.ts +89 -0
- package/dist/lib/terminal/preferred.js +87 -0
- package/dist/lib/terminal/run-surface.d.ts +82 -0
- package/dist/lib/terminal/run-surface.js +146 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents projects` — named, multi-repo projects and the progress
|
|
3
|
+
* rollup. Definitions live in `~/.agents/projects/<name>.yaml` (see
|
|
4
|
+
* `lib/projects.ts`); this registers the command tree over them. Beta-gated on
|
|
5
|
+
* `isBetaEnabled('projects')`, mirroring `agents factory`.
|
|
6
|
+
*
|
|
7
|
+
* The headline is `status`: instead of the vague per-agent activity line, it
|
|
8
|
+
* rolls every session up by project (matched on cwd) into one card — agents by
|
|
9
|
+
* lifecycle state, plan completion, open PRs, and tickets in flight.
|
|
10
|
+
*/
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import { execFileSync, spawnSync } from 'child_process';
|
|
14
|
+
import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
|
|
15
|
+
import { setHelpSections } from '../lib/help.js';
|
|
16
|
+
import { getMainRepoRoot } from '../lib/git.js';
|
|
17
|
+
import { parseOwnerRepoFromRemote } from '../lib/registry.js';
|
|
18
|
+
import { toHomeRelative } from '../lib/project-root.js';
|
|
19
|
+
import { getActiveSessions } from '../lib/session/active.js';
|
|
20
|
+
import { factoryProjectsPath } from '../lib/auto-dispatch.js';
|
|
21
|
+
import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, projectDefPath, isSafeProjectName, } from '../lib/projects.js';
|
|
22
|
+
import { rollupSessionsByProject, planPct, enrichProjectSignals, } from '../lib/project-status.js';
|
|
23
|
+
/** `path:purpose` → a context anchor. Purpose may contain colons. */
|
|
24
|
+
function parseContextFlag(raw) {
|
|
25
|
+
const i = raw.indexOf(':');
|
|
26
|
+
if (i === -1)
|
|
27
|
+
return { path: raw.trim(), purpose: '' };
|
|
28
|
+
return { path: raw.slice(0, i).trim(), purpose: raw.slice(i + 1).trim() };
|
|
29
|
+
}
|
|
30
|
+
/** Best-effort `owner/repo` from a repo's origin remote. */
|
|
31
|
+
function originSlug(cwd) {
|
|
32
|
+
try {
|
|
33
|
+
const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd, encoding: 'utf8' }).trim();
|
|
34
|
+
return parseOwnerRepoFromRemote(url) ?? undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function statusBar(r) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
const push = (n, label, color) => {
|
|
43
|
+
if (n && n > 0)
|
|
44
|
+
parts.push(color(`${n} ${label}`));
|
|
45
|
+
};
|
|
46
|
+
push(r.byStatus.running, 'running', chalk.green);
|
|
47
|
+
push(r.byStatus.idle, 'idle', chalk.gray);
|
|
48
|
+
push(r.byStatus.input_required, 'need-input', chalk.yellow);
|
|
49
|
+
push(r.byStatus.queued, 'queued', chalk.gray);
|
|
50
|
+
const shown = (r.byStatus.running ?? 0) + (r.byStatus.idle ?? 0) + (r.byStatus.input_required ?? 0) + (r.byStatus.queued ?? 0);
|
|
51
|
+
// Sessions in a non-live state (orphaned/crashed/unknown) count toward the
|
|
52
|
+
// headline — render the remainder so the bar never sums to less than it.
|
|
53
|
+
if (r.agents > shown)
|
|
54
|
+
parts.push(chalk.gray(`+${r.agents - shown} other`));
|
|
55
|
+
return parts.join(' · ') || chalk.gray('no live agents');
|
|
56
|
+
}
|
|
57
|
+
function renderCard(def, r, remote) {
|
|
58
|
+
const agents = r?.agents ?? 0;
|
|
59
|
+
const pct = r ? planPct(r.plan) : undefined;
|
|
60
|
+
const planStr = pct === undefined ? '' : ` · ${chalk.cyan(`${pct}% plan`)}`;
|
|
61
|
+
console.log(`${chalk.bold(def.name)} ${chalk.dim('·')} ${chalk.bold(`${agents} agents`)}${planStr}`);
|
|
62
|
+
if (def.description)
|
|
63
|
+
console.log(` ${chalk.dim(def.description)}`);
|
|
64
|
+
console.log(` ${chalk.dim('live')} ${r ? statusBar(r) : chalk.gray('no live agents')}`);
|
|
65
|
+
const ships = [];
|
|
66
|
+
if (remote?.mergedPrs)
|
|
67
|
+
ships.push(chalk.green(`${remote.mergedPrs} merged (${remote.windowDays}d)`));
|
|
68
|
+
if (r?.openPrs.length)
|
|
69
|
+
ships.push(`${r.openPrs.length} open PR${r.openPrs.length === 1 ? '' : 's'}`);
|
|
70
|
+
if (r?.worktrees)
|
|
71
|
+
ships.push(`${r.worktrees} worktree${r.worktrees === 1 ? '' : 's'}`);
|
|
72
|
+
if (ships.length)
|
|
73
|
+
console.log(` ${chalk.dim('ships')} ${ships.join(' · ')}`);
|
|
74
|
+
if (r && r.tickets.length) {
|
|
75
|
+
console.log(` ${chalk.dim('tickets')} ${r.tickets.slice(0, 8).join(' · ')}${r.tickets.length > 8 ? ' …' : ''}`);
|
|
76
|
+
}
|
|
77
|
+
if (remote?.artifacts) {
|
|
78
|
+
const last = remote.lastArtifact ? ` ${chalk.dim(`· last: ${remote.lastArtifact}`)}` : '';
|
|
79
|
+
console.log(` ${chalk.dim('proof')} ${remote.artifacts} artifact${remote.artifacts === 1 ? '' : 's'} (${remote.windowDays}d)${last}`);
|
|
80
|
+
}
|
|
81
|
+
const repos = [def.repo, ...(def.repos ?? []).map((x) => x.slug)].filter(Boolean);
|
|
82
|
+
if (repos.length)
|
|
83
|
+
console.log(` ${chalk.dim('repos')} ${[...new Set(repos)].join(' · ')}`);
|
|
84
|
+
if (def.contexts?.length) {
|
|
85
|
+
console.log(` ${chalk.dim('context')} ${def.contexts.map((c) => c.path).join(' · ')}`);
|
|
86
|
+
}
|
|
87
|
+
if (def.integrations?.length) {
|
|
88
|
+
console.log(` ${chalk.dim('links')} ${def.integrations.map((i) => i.label ?? i.kind).join(' · ')}`);
|
|
89
|
+
}
|
|
90
|
+
console.log('');
|
|
91
|
+
}
|
|
92
|
+
export function registerProjectsCommands(program) {
|
|
93
|
+
const enabled = isBetaEnabled('projects');
|
|
94
|
+
const projects = program
|
|
95
|
+
.command('projects', { hidden: !enabled })
|
|
96
|
+
.description('Named multi-repo projects with a progress rollup.');
|
|
97
|
+
setHelpSections(projects, {
|
|
98
|
+
examples: `
|
|
99
|
+
agents projects add rush --repo phnx-labs/rush --path apps/web
|
|
100
|
+
agents projects list
|
|
101
|
+
agents projects status # progress card for every project
|
|
102
|
+
agents projects status rush --json # one project, machine-readable
|
|
103
|
+
agents run --project rush # land an agent in the project
|
|
104
|
+
`,
|
|
105
|
+
notes: `
|
|
106
|
+
Definitions are hand-editable YAML in ~/.agents/projects/ and sync across
|
|
107
|
+
machines with 'agents push/pull'. Enable with: agents beta enable projects.
|
|
108
|
+
`,
|
|
109
|
+
});
|
|
110
|
+
projects.hook('preAction', () => {
|
|
111
|
+
if (enabled)
|
|
112
|
+
return;
|
|
113
|
+
console.error(chalk.red('agents projects is in beta.'));
|
|
114
|
+
console.error(chalk.gray(betaEnableHint('projects')));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
});
|
|
117
|
+
// ---- list ----
|
|
118
|
+
projects
|
|
119
|
+
.command('list')
|
|
120
|
+
.description('List defined projects with their root, repo, and live agent count.')
|
|
121
|
+
.option('--json', 'Machine-readable output')
|
|
122
|
+
.action(async (opts) => {
|
|
123
|
+
const defs = listProjectDefs();
|
|
124
|
+
if (opts.json) {
|
|
125
|
+
const roll = rollupSessionsByProject(defs, await getActiveSessions());
|
|
126
|
+
console.log(JSON.stringify(defs.map((d) => ({ ...d, agents: roll.get(d.name)?.agents ?? 0 })), null, 2));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!defs.length) {
|
|
130
|
+
console.log(chalk.gray('No projects defined. Add one: agents projects add <name>'));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const roll = rollupSessionsByProject(defs, await getActiveSessions());
|
|
134
|
+
for (const d of defs) {
|
|
135
|
+
const agents = roll.get(d.name)?.agents ?? 0;
|
|
136
|
+
const repo = d.repo ?? d.repos?.[0]?.slug ?? '';
|
|
137
|
+
console.log(` ${chalk.bold(d.name.padEnd(16))} ${chalk.dim((d.root ?? d.defaultPath ?? '').padEnd(32))} ${chalk.cyan(repo.padEnd(24))} ${agents} agents`);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// ---- add ----
|
|
141
|
+
projects
|
|
142
|
+
.command('add <name>')
|
|
143
|
+
.description('Define a project. Infers root and repo from the current git repo when not given.')
|
|
144
|
+
.option('--root <path>', 'Repo / monorepo root (defaults to the current git repo root)')
|
|
145
|
+
.option('--path <subdir>', 'Default cwd for agents (a monorepo subdir)')
|
|
146
|
+
.option('--repo <owner/repo>', 'Primary GitHub slug (defaults to the origin remote)')
|
|
147
|
+
.option('--context <path:purpose...>', 'A described starting point; repeatable')
|
|
148
|
+
.option('--linear <url-or-id>', 'Linear project URL or id')
|
|
149
|
+
.option('--force', 'Overwrite an existing definition')
|
|
150
|
+
.action(async (name, opts) => {
|
|
151
|
+
if (!isSafeProjectName(name)) {
|
|
152
|
+
console.error(chalk.red(`Invalid project name: "${name}" (letters, digits, ., _, - only)`));
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
if (loadProjectDef(name) && !opts.force) {
|
|
156
|
+
console.error(chalk.red(`Project "${name}" already exists. Use --force to overwrite, or 'agents projects edit ${name}'.`));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
const cwd = process.cwd();
|
|
160
|
+
let root = opts.root;
|
|
161
|
+
if (!root) {
|
|
162
|
+
try {
|
|
163
|
+
root = toHomeRelative(await getMainRepoRoot(cwd));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
console.error(chalk.red('Not inside a git repo — pass --root <path> explicitly.'));
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const repo = opts.repo ?? originSlug(cwd);
|
|
171
|
+
const def = { name, root };
|
|
172
|
+
if (opts.path)
|
|
173
|
+
def.defaultPath = `${root.replace(/\/$/, '')}/${opts.path.replace(/^\//, '')}`;
|
|
174
|
+
if (repo)
|
|
175
|
+
def.repo = repo;
|
|
176
|
+
if (opts.context?.length)
|
|
177
|
+
def.contexts = opts.context.map(parseContextFlag);
|
|
178
|
+
if (opts.linear) {
|
|
179
|
+
def.linear = /^https?:/.test(opts.linear) ? { url: opts.linear } : { projectId: opts.linear };
|
|
180
|
+
}
|
|
181
|
+
const target = writeProjectDef(def);
|
|
182
|
+
console.log(chalk.green(`Defined project "${name}"`));
|
|
183
|
+
console.log(chalk.gray(` ${target}`));
|
|
184
|
+
console.log(chalk.gray(` root ${def.root}${def.repo ? ` · repo ${def.repo}` : ''}`));
|
|
185
|
+
});
|
|
186
|
+
// ---- show ----
|
|
187
|
+
projects
|
|
188
|
+
.command('show <name>')
|
|
189
|
+
.description('Show a project definition, resolved paths, repos, contexts, and links.')
|
|
190
|
+
.option('--json', 'Machine-readable output')
|
|
191
|
+
.action((name, opts) => {
|
|
192
|
+
const def = loadProjectDef(name);
|
|
193
|
+
if (!def) {
|
|
194
|
+
console.error(chalk.red(`No project named "${name}". List them: agents projects list`));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
if (opts.json) {
|
|
198
|
+
console.log(JSON.stringify(def, null, 2));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
console.log(chalk.bold(def.name) + (def.description ? chalk.dim(` — ${def.description}`) : ''));
|
|
202
|
+
if (def.root)
|
|
203
|
+
console.log(` root ${def.root}`);
|
|
204
|
+
if (def.defaultPath)
|
|
205
|
+
console.log(` defaultPath ${def.defaultPath}`);
|
|
206
|
+
const repos = [def.repo, ...(def.repos ?? []).map((r) => (r.subpath ? `${r.slug} (${r.subpath})` : r.slug))].filter(Boolean);
|
|
207
|
+
if (repos.length)
|
|
208
|
+
console.log(` repos ${repos.join(', ')}`);
|
|
209
|
+
for (const c of def.contexts ?? [])
|
|
210
|
+
console.log(` context ${chalk.cyan(c.path)} — ${c.purpose}`);
|
|
211
|
+
for (const i of def.integrations ?? [])
|
|
212
|
+
console.log(` ${i.kind.padEnd(12)} ${i.url}${i.label ? chalk.dim(` (${i.label})`) : ''}`);
|
|
213
|
+
if (def.linear?.url || def.linear?.projectId)
|
|
214
|
+
console.log(` linear ${def.linear.url ?? def.linear.projectId}`);
|
|
215
|
+
for (const d of def.docs ?? [])
|
|
216
|
+
console.log(` doc ${d}`);
|
|
217
|
+
console.log(chalk.gray(` ${projectDefPath(name)}`));
|
|
218
|
+
});
|
|
219
|
+
// ---- edit ----
|
|
220
|
+
projects
|
|
221
|
+
.command('edit <name>')
|
|
222
|
+
.description('Open the project YAML in $EDITOR (it is hand-editable regardless).')
|
|
223
|
+
.action((name) => {
|
|
224
|
+
const target = projectDefPath(name);
|
|
225
|
+
if (!fs.existsSync(target)) {
|
|
226
|
+
console.error(chalk.red(`No project named "${name}". Create it: agents projects add ${name}`));
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const editor = process.env.VISUAL || process.env.EDITOR || 'vi';
|
|
230
|
+
// $EDITOR commonly carries args ("code --wait") — split like monitors/routines do.
|
|
231
|
+
const parts = editor.split(/\s+/).filter(Boolean);
|
|
232
|
+
const res = spawnSync(parts[0], [...parts.slice(1), target], { stdio: 'inherit' });
|
|
233
|
+
process.exit(res.status ?? 0);
|
|
234
|
+
});
|
|
235
|
+
// ---- status ----
|
|
236
|
+
projects
|
|
237
|
+
.command('status [name]')
|
|
238
|
+
.description('Progress rollup: agents, plan %, merged/open PRs, tickets, and artifacts per project.')
|
|
239
|
+
.option('--json', 'Machine-readable output')
|
|
240
|
+
.option('--window <days>', 'Window for merged PRs and artifacts', '7')
|
|
241
|
+
.option('--no-remote', 'Skip the GitHub lookup (merged-PR count); faster, offline')
|
|
242
|
+
.action(async (name, opts) => {
|
|
243
|
+
const all = listProjectDefs();
|
|
244
|
+
// Named lookup goes through the strict single-def loader so a broken
|
|
245
|
+
// <name>.yaml surfaces its validation error instead of "No project named".
|
|
246
|
+
const defs = name ? [loadProjectDef(name)].filter((d) => d !== undefined) : all;
|
|
247
|
+
if (name && !defs.length) {
|
|
248
|
+
console.error(chalk.red(`No project named "${name}".`));
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
if (!defs.length) {
|
|
252
|
+
if (opts.json)
|
|
253
|
+
console.log('[]');
|
|
254
|
+
else
|
|
255
|
+
console.log(chalk.gray('No projects defined. Add one: agents projects add <name>'));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const windowDays = Math.max(1, Number.parseInt(opts.window ?? '7', 10) || 7);
|
|
259
|
+
const nowMs = Date.now();
|
|
260
|
+
const roll = rollupSessionsByProject(all, await getActiveSessions());
|
|
261
|
+
// Enrich only the shown projects. --no-remote still reads the local artifact
|
|
262
|
+
// log but skips the gh call (def.repo left unused → mergedPrs stays 0).
|
|
263
|
+
const remote = new Map();
|
|
264
|
+
await Promise.all(defs.map(async (d) => {
|
|
265
|
+
remote.set(d.name, await enrichProjectSignals(d, windowDays, nowMs, { skipRemote: opts.remote === false }));
|
|
266
|
+
}));
|
|
267
|
+
if (opts.json) {
|
|
268
|
+
console.log(JSON.stringify(defs.map((d) => {
|
|
269
|
+
const r = roll.get(d.name);
|
|
270
|
+
const rem = remote.get(d.name);
|
|
271
|
+
return {
|
|
272
|
+
name: d.name,
|
|
273
|
+
agents: r?.agents ?? 0,
|
|
274
|
+
byStatus: r?.byStatus ?? {},
|
|
275
|
+
plan: r?.plan ?? { done: 0, total: 0 },
|
|
276
|
+
planPct: r ? planPct(r.plan) ?? null : null,
|
|
277
|
+
openPrs: r?.openPrs ?? [],
|
|
278
|
+
mergedPrs: rem?.mergedPrs ?? 0,
|
|
279
|
+
tickets: r?.tickets ?? [],
|
|
280
|
+
worktrees: r?.worktrees ?? 0,
|
|
281
|
+
artifacts: rem?.artifacts ?? 0,
|
|
282
|
+
lastArtifact: rem?.lastArtifact ?? null,
|
|
283
|
+
windowDays,
|
|
284
|
+
repos: [d.repo, ...(d.repos ?? []).map((r2) => r2.slug)].filter(Boolean),
|
|
285
|
+
};
|
|
286
|
+
}), null, 2));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
for (const d of defs)
|
|
290
|
+
renderCard(d, roll.get(d.name), remote.get(d.name));
|
|
291
|
+
});
|
|
292
|
+
// ---- import ----
|
|
293
|
+
projects
|
|
294
|
+
.command('import')
|
|
295
|
+
.description('Absorb the Factory project registry (~/.agents/factory/projects.json) into YAML definitions.')
|
|
296
|
+
.requiredOption('--from-factory', 'Import from the Factory projects.json registry')
|
|
297
|
+
.option('--force', 'Overwrite existing definitions')
|
|
298
|
+
.action((opts) => {
|
|
299
|
+
const src = factoryProjectsPath();
|
|
300
|
+
let rawText;
|
|
301
|
+
try {
|
|
302
|
+
rawText = fs.readFileSync(src, 'utf8');
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
console.error(chalk.red(`No Factory registry at ${src}`));
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
let rows;
|
|
309
|
+
try {
|
|
310
|
+
rows = JSON.parse(rawText);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
console.error(chalk.red(`Factory registry at ${src} is not valid JSON`));
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
const list = Array.isArray(rows) ? rows : Array.isArray(rows.projects) ? rows.projects : [];
|
|
317
|
+
let created = 0;
|
|
318
|
+
let skipped = 0;
|
|
319
|
+
for (const raw of list) {
|
|
320
|
+
if (!raw || typeof raw !== 'object')
|
|
321
|
+
continue;
|
|
322
|
+
const o = raw;
|
|
323
|
+
const name = typeof o.name === 'string' ? o.name : undefined;
|
|
324
|
+
if (!name || !isSafeProjectName(name)) {
|
|
325
|
+
skipped++;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (loadProjectDef(name) && !opts.force) {
|
|
329
|
+
skipped++;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const def = { name };
|
|
333
|
+
if (typeof o.path === 'string')
|
|
334
|
+
def.root = toHomeRelative(o.path);
|
|
335
|
+
if (typeof o.repoSlug === 'string')
|
|
336
|
+
def.repo = o.repoSlug;
|
|
337
|
+
if (typeof o.linearProjectId === 'string')
|
|
338
|
+
def.linear = { projectId: o.linearProjectId };
|
|
339
|
+
writeProjectDef(def);
|
|
340
|
+
created++;
|
|
341
|
+
}
|
|
342
|
+
console.log(chalk.green(`Imported ${created} project${created === 1 ? '' : 's'}${skipped ? chalk.gray(` (${skipped} skipped)`) : ''}`));
|
|
343
|
+
});
|
|
344
|
+
// ---- rm ----
|
|
345
|
+
projects
|
|
346
|
+
.command('rm <name>')
|
|
347
|
+
.alias('remove')
|
|
348
|
+
.description('Delete a project definition. Never touches the repo.')
|
|
349
|
+
.action((name) => {
|
|
350
|
+
if (removeProjectDef(name)) {
|
|
351
|
+
console.log(chalk.green(`Removed project "${name}"`));
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
console.error(chalk.red(`No project named "${name}".`));
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
@@ -19,9 +19,10 @@ import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js'
|
|
|
19
19
|
import { getRoutinesDir } from '../lib/state.js';
|
|
20
20
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
21
21
|
import { safeJoin } from '../lib/paths.js';
|
|
22
|
-
import { executeJob,
|
|
22
|
+
import { executeJob, monitorRunningJobs, ROUTINE_AGENT_IDS } from '../lib/runner.js';
|
|
23
23
|
import { JobScheduler } from '../lib/scheduler.js';
|
|
24
24
|
import { detectOverdueJobs } from '../lib/overdue.js';
|
|
25
|
+
import { runCatchup } from '../lib/catchup.js';
|
|
25
26
|
import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
26
27
|
import { setHelpSections } from '../lib/help.js';
|
|
27
28
|
import { loadDevices, loadDevicesSync } from '../lib/devices/registry.js';
|
|
@@ -242,7 +243,10 @@ function renderRoutineRows({ jobs, scheduler, overdueSet, link, now, local = tru
|
|
|
242
243
|
const statusColor = lastStatus === 'completed' ? chalk.green
|
|
243
244
|
: lastStatus === 'failed' ? chalk.red
|
|
244
245
|
: lastStatus === 'timeout' ? chalk.yellow
|
|
245
|
-
|
|
246
|
+
// A miss is an infrastructure problem, not a task failure — the routine
|
|
247
|
+
// never ran. Distinct from red so the two prompt different reactions.
|
|
248
|
+
: lastStatus === 'missed' ? chalk.magenta
|
|
249
|
+
: chalk.gray;
|
|
246
250
|
const overdueTag = overdueSet.has(job.name) ? chalk.yellow(' (overdue)') : '';
|
|
247
251
|
const agentLabelPadded = job.command
|
|
248
252
|
? chalk.magenta('command'.padEnd(10))
|
|
@@ -544,6 +548,7 @@ export function registerRoutinesCommands(program) {
|
|
|
544
548
|
sourceRepo: job.source?.repo ?? job.repo ?? null,
|
|
545
549
|
sourceBranch: job.source?.branch ?? null,
|
|
546
550
|
runOnce: Boolean(job.runOnce),
|
|
551
|
+
catchup: job.catchup !== false,
|
|
547
552
|
oneShot: isOneShotRoutine(job),
|
|
548
553
|
expired: isPastOneShotRoutine(job, nowJson),
|
|
549
554
|
runsHere: jobRunsOnThisDevice(job),
|
|
@@ -622,6 +627,7 @@ export function registerRoutinesCommands(program) {
|
|
|
622
627
|
.option('--state-to <name>', 'Linear current-state filter for --on linear:<event> (e.g. Plan)')
|
|
623
628
|
.option('--state-from <name>', 'Linear previous-state filter for --on linear:<event> (e.g. Triage)')
|
|
624
629
|
.option('--end-at <iso>', 'Stop firing on or after this ISO 8601 timestamp (e.g., "2026-12-31T23:59:00Z"); routine auto-disables.')
|
|
630
|
+
.option('--no-catchup', 'Do not run this routine late if its fire is missed (daemon down/asleep). The miss is still recorded. For routines whose value expires with their slot, e.g. a 9am brief.')
|
|
625
631
|
.option('--disabled', 'Create the routine but keep it paused (enable later with resume)')
|
|
626
632
|
.option('--resume <sessionId>', 'At fire time, resume this existing session id (via `agents run <agent> --resume`) instead of starting fresh — the actual session reopens with full context and the prompt becomes its next turn. Powers self-scheduled wake-ups (e.g. /hibernate). Requires --agent claude or codex; runs un-sandboxed (the session store lives in the real home, not the job overlay).')
|
|
627
633
|
.option('--json', 'Emit machine-readable JSON with the created routine id and status')
|
|
@@ -734,6 +740,7 @@ export function registerRoutinesCommands(program) {
|
|
|
734
740
|
...(hostStrategy ? { hostStrategy } : {}),
|
|
735
741
|
...(options.runCwd ? { remoteCwd: options.runCwd } : {}),
|
|
736
742
|
...(runOnce ? { runOnce: true } : {}),
|
|
743
|
+
...(options.catchup === false ? { catchup: false } : {}),
|
|
737
744
|
...(options.endAt ? { endAt: options.endAt } : {}),
|
|
738
745
|
...(options.resume ? { resume: options.resume } : {}),
|
|
739
746
|
};
|
|
@@ -1109,44 +1116,42 @@ export function registerRoutinesCommands(program) {
|
|
|
1109
1116
|
});
|
|
1110
1117
|
routinesCmd
|
|
1111
1118
|
.command('catchup')
|
|
1112
|
-
.description('Run any routines that missed their last scheduled fire
|
|
1113
|
-
.option('--dry-run', '
|
|
1119
|
+
.description('Run any routines that missed their last scheduled fire on demand. The daemon already does this every 5 minutes — use this to force a pass now. Detached: runs in the background under the scheduler.')
|
|
1120
|
+
.option('--dry-run', 'Record the misses and list them without running anything')
|
|
1114
1121
|
.action(async (options) => {
|
|
1115
1122
|
const overdue = detectOverdueJobs();
|
|
1116
1123
|
if (overdue.length === 0) {
|
|
1117
|
-
console.log(chalk.gray('No
|
|
1124
|
+
console.log(chalk.gray('No missed fires.'));
|
|
1118
1125
|
return;
|
|
1119
1126
|
}
|
|
1120
|
-
console.log(chalk.bold(`${overdue.length}
|
|
1127
|
+
console.log(chalk.bold(`${overdue.length} missed fire(s):\n`));
|
|
1121
1128
|
for (const job of overdue) {
|
|
1122
1129
|
const last = job.lastRanAt ? job.lastRanAt.toLocaleString() : 'never';
|
|
1123
1130
|
console.log(` ${chalk.cyan(job.name)} — missed ${chalk.gray(job.expectedAt.toLocaleString())}, last ran ${chalk.gray(last)}`);
|
|
1124
1131
|
}
|
|
1125
|
-
if (options.dryRun) {
|
|
1126
|
-
console.log(chalk.gray('\n(dry run — no jobs triggered)'));
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
1132
|
// Need the daemon alive so spawned jobs are monitored and meta.json is
|
|
1130
1133
|
// finalized. Start it if it isn't already running.
|
|
1131
|
-
if (!isDaemonRunning()) {
|
|
1134
|
+
if (!options.dryRun && !isDaemonRunning()) {
|
|
1132
1135
|
const started = startDaemon();
|
|
1133
1136
|
if (started.pid) {
|
|
1134
1137
|
console.log(chalk.gray(`\nStarted scheduler (PID: ${started.pid}) so catchup runs are monitored.`));
|
|
1135
1138
|
}
|
|
1136
1139
|
}
|
|
1137
|
-
console.log(chalk.bold('\nTriggering catchup runs...'));
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if (
|
|
1141
|
-
console.log(` ${
|
|
1142
|
-
continue;
|
|
1140
|
+
console.log(chalk.bold(options.dryRun ? '\nRecording misses...' : '\nTriggering catchup runs...'));
|
|
1141
|
+
const outcomes = await runCatchup({ overdue, dryRun: options.dryRun });
|
|
1142
|
+
for (const o of outcomes) {
|
|
1143
|
+
if (o.result === 'ran') {
|
|
1144
|
+
console.log(` ${o.name} → ${chalk.green('started')} (run: ${o.runId})`);
|
|
1143
1145
|
}
|
|
1144
|
-
|
|
1145
|
-
const
|
|
1146
|
-
console.log(` ${
|
|
1146
|
+
else if (o.result === 'recorded') {
|
|
1147
|
+
const why = options.dryRun ? 'dry run' : 'catchup: false';
|
|
1148
|
+
console.log(` ${o.name} → ${chalk.yellow('recorded as missed')} (${why})`);
|
|
1147
1149
|
}
|
|
1148
|
-
|
|
1149
|
-
console.log(` ${
|
|
1150
|
+
else if (o.result === 'claimed-elsewhere') {
|
|
1151
|
+
console.log(` ${o.name} → ${chalk.gray('already claimed by the scheduler')}`);
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
console.log(` ${o.name} → ${chalk.red('failed to start')}: ${o.error}`);
|
|
1150
1155
|
}
|
|
1151
1156
|
}
|
|
1152
1157
|
console.log(chalk.gray('\nTrack progress with: agents routines runs <name>'));
|
|
@@ -1270,8 +1275,9 @@ export function registerRoutinesCommands(program) {
|
|
|
1270
1275
|
// is the concise view. Falls back to a bounded stdout tail when no report
|
|
1271
1276
|
// was extracted (e.g. the run failed before finishing).
|
|
1272
1277
|
const statusColor = run.status === 'completed' ? chalk.green
|
|
1273
|
-
: run.status === '
|
|
1274
|
-
: chalk.
|
|
1278
|
+
: run.status === 'missed' ? chalk.magenta
|
|
1279
|
+
: run.status === 'failed' || run.status === 'timeout' ? chalk.red
|
|
1280
|
+
: chalk.yellow;
|
|
1275
1281
|
console.log(chalk.bold(name) + chalk.gray(` run ${runId}`));
|
|
1276
1282
|
console.log(statusColor(run.status) +
|
|
1277
1283
|
chalk.gray(` ${run.startedAt}`) +
|
|
@@ -9,6 +9,7 @@ import { type Command } from 'commander';
|
|
|
9
9
|
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
10
10
|
import { quoteWin32ExecArg } from '../lib/platform/index.js';
|
|
11
11
|
import { type SecretsBackend, type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.js';
|
|
12
|
+
import { type SecretsListFilterOpts } from '../lib/secrets/list-filter.js';
|
|
12
13
|
/** Read all available data from stdin synchronously, trimmed. */
|
|
13
14
|
/**
|
|
14
15
|
* Read the raw `.env` text for `import --from <path|->`. A `-` reads the .env
|
|
@@ -51,6 +52,21 @@ export declare function buildRemoteUnlockArgs(names: string[], opts: {
|
|
|
51
52
|
ttl?: string;
|
|
52
53
|
durable?: boolean;
|
|
53
54
|
}): string[];
|
|
55
|
+
/**
|
|
56
|
+
* Build the remote `agents secrets list` argv for `list --host`. Every filter
|
|
57
|
+
* must be forwarded: `browseRemote` sends this argv verbatim, so a flag left out
|
|
58
|
+
* here is not an error — the remote just lists everything, and
|
|
59
|
+
* `secrets list --host zion --expired` reports every bundle on zion as expired.
|
|
60
|
+
* Silent and wrong beats loud and wrong only for the person who wrote the bug.
|
|
61
|
+
*
|
|
62
|
+
* Values pass through unparsed so the REMOTE validates them under its own rules,
|
|
63
|
+
* matching how `--ttl` is forwarded by buildRemoteUnlockArgs.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildRemoteListArgs(opts: SecretsListFilterOpts & {
|
|
66
|
+
json?: boolean;
|
|
67
|
+
sort?: string;
|
|
68
|
+
limit?: string;
|
|
69
|
+
}, query?: string): string[];
|
|
54
70
|
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
55
71
|
/**
|
|
56
72
|
* Build the child environment for `agents secrets exec`. Strips
|
|
@@ -79,11 +95,19 @@ export declare function exportBundleToFile(env: Record<string, string>, filePath
|
|
|
79
95
|
* Throws on a missing file, an invalid JSON envelope, or a wrong passphrase.
|
|
80
96
|
*/
|
|
81
97
|
export declare function importBundleFromFile(filePath: string, passphrase: string): Record<string, string>;
|
|
82
|
-
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
98
|
+
/** Compact span for a fixed duration: "45m" / "19h" / "2d". Shared by
|
|
99
|
+
* `compactRemaining` (a countdown) and the POLICY column's hold-window
|
|
100
|
+
* annotation (a fixed length) so the two can never round onto different unit
|
|
101
|
+
* thresholds and disagree about the same number of milliseconds. */
|
|
102
|
+
export declare function compactDurationMs(ms: number): string;
|
|
103
|
+
/** The POLICY column for `secrets list`. `hold` is a duration, not a mode — it
|
|
104
|
+
* means "prompt once, then stay silent for this long" — so the column states
|
|
105
|
+
* the window (`hold 7d`) rather than the bare tier name, and appends `· held Nd`
|
|
106
|
+
* while the secrets-agent is actually caching the bundle. `always` and `never`
|
|
107
|
+
* carry no window and gain nothing here. `holdMs` is the configured global hold
|
|
108
|
+
* (`secretsHoldMs()`), passed in rather than read so this stays pure; `held`
|
|
85
109
|
* maps bundle name → expiry epoch-ms (from agentStatus()). */
|
|
86
|
-
export declare function renderPolicyCol(b: SecretsBundle, held?: Map<string, number>): string;
|
|
110
|
+
export declare function renderPolicyCol(b: SecretsBundle, holdMs: number, held?: Map<string, number>): string;
|
|
87
111
|
/** The hold-window line at the top of `secrets status`. Names the `hold` policy
|
|
88
112
|
* the window belongs to — the rename in #1604 left this surface still saying
|
|
89
113
|
* "daily", the one name the CLI no longer accepts in its own help. Pure so the
|
|
@@ -97,6 +121,21 @@ export declare const NO_BUNDLES_HELD_LINE = "No bundles held. The next read of e
|
|
|
97
121
|
* minutes (so a near-floor `holdMs` never shows a confusing "0 hours"), whole
|
|
98
122
|
* hours up to 2 days, whole days beyond. Pure — unit-tested. */
|
|
99
123
|
export declare function formatHoldWindow(ms: number): string;
|
|
124
|
+
/** A broker hold expiry, or null once it has lapsed. One definition of "held",
|
|
125
|
+
* shared by the POLICY column, the `--held` filter, and the JSON payload, so the
|
|
126
|
+
* three can never disagree about the same bundle. */
|
|
127
|
+
export declare function liveHold(expiresAt: number | undefined, now?: number): number | null;
|
|
128
|
+
/**
|
|
129
|
+
* The EXPIRING cell. Counts keys needing attention — already expired plus due
|
|
130
|
+
* within 30 days — and colours by the worst of the two: red once anything has
|
|
131
|
+
* lapsed, yellow while everything is merely upcoming.
|
|
132
|
+
*
|
|
133
|
+
* Expired keys used to be invisible here. `countExpiringSoon` requires
|
|
134
|
+
* `d >= 0`, so a bundle whose token died last month rendered `-`, identical to
|
|
135
|
+
* one with no expiry at all; the only places it surfaced were `secrets view`
|
|
136
|
+
* and a hard abort at inject time, i.e. after it had already broken something.
|
|
137
|
+
*/
|
|
138
|
+
export declare function renderExpiringCol(b: SecretsBundle, now?: number): string;
|
|
100
139
|
/** Register the `agents secrets` command tree. */
|
|
101
140
|
export declare function registerSecretsCommands(program: Command): void;
|
|
102
141
|
/** Validate a prompt-policy value, throwing a clear message on a bad one (the
|