@phnx-labs/agents-cli 1.22.6 → 1.22.8
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 +131 -1
- package/README.md +7 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +61 -0
- package/dist/commands/exec.js +83 -16
- package/dist/commands/feed.d.ts +2 -1
- package/dist/commands/feed.js +47 -20
- package/dist/commands/focus.js +22 -1
- package/dist/commands/harness.d.ts +0 -1
- package/dist/commands/harness.js +60 -4
- package/dist/commands/models.js +2 -2
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/projects.js +122 -107
- package/dist/commands/routines.js +2 -2
- package/dist/commands/run-account-picker.js +2 -0
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/sessions-backfill.d.ts +33 -0
- package/dist/commands/sessions-backfill.js +83 -1
- package/dist/commands/sessions-stats.d.ts +36 -0
- package/dist/commands/sessions-stats.js +263 -0
- package/dist/commands/sessions.d.ts +1 -1
- package/dist/commands/sessions.js +21 -1
- package/dist/commands/snapshot.d.ts +11 -0
- package/dist/commands/snapshot.js +107 -0
- package/dist/commands/teams.js +2 -1
- package/dist/commands/view.d.ts +9 -1
- package/dist/commands/view.js +63 -12
- package/dist/index.js +4 -2
- package/dist/lib/activity.js +2 -2
- package/dist/lib/agents.js +68 -11
- package/dist/lib/analytics/recipes.js +11 -5
- package/dist/lib/browser/ipc.js +2 -0
- package/dist/lib/browser/profiles.d.ts +15 -7
- package/dist/lib/browser/profiles.js +53 -12
- package/dist/lib/browser/remote-control.d.ts +35 -0
- package/dist/lib/browser/remote-control.js +48 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +19 -1
- package/dist/lib/browser/types.d.ts +14 -2
- package/dist/lib/byok-usage.d.ts +38 -0
- package/dist/lib/byok-usage.js +117 -0
- package/dist/lib/capabilities.js +1 -1
- package/dist/lib/device-config.js +8 -0
- package/dist/lib/exec.d.ts +20 -0
- package/dist/lib/exec.js +73 -8
- package/dist/lib/feed-outcome.d.ts +1 -0
- package/dist/lib/feed-outcome.js +2 -0
- package/dist/lib/feed-post.js +1 -1
- package/dist/lib/feed-ranking.d.ts +1 -0
- package/dist/lib/feed-ranking.js +4 -0
- package/dist/lib/feed.d.ts +4 -1
- package/dist/lib/feed.js +25 -0
- package/dist/lib/hosts/passthrough.d.ts +10 -1
- package/dist/lib/hosts/passthrough.js +23 -3
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/mcp.js +6 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.js +4 -1
- package/dist/lib/models.js +63 -0
- package/dist/lib/placement.d.ts +82 -0
- package/dist/lib/placement.js +188 -0
- package/dist/lib/profiles.d.ts +73 -10
- package/dist/lib/profiles.js +133 -15
- package/dist/lib/project-focus.d.ts +9 -0
- package/dist/lib/project-focus.js +23 -0
- package/dist/lib/project-key.d.ts +1 -1
- package/dist/lib/project-key.js +1 -1
- package/dist/lib/project-probe.d.ts +18 -0
- package/dist/lib/project-probe.js +46 -0
- package/dist/lib/project-status.d.ts +56 -0
- package/dist/lib/project-status.js +125 -18
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/rotate.d.ts +19 -4
- package/dist/lib/rotate.js +24 -1
- package/dist/lib/routines.d.ts +2 -0
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.js +90 -7
- 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.js +1 -1
- package/dist/lib/secrets/index.d.ts +1 -0
- package/dist/lib/secrets/index.js +29 -15
- package/dist/lib/secrets/remote.d.ts +1 -1
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/session/active.d.ts +2 -0
- package/dist/lib/session/bash-command.d.ts +2 -3
- package/dist/lib/session/db.d.ts +92 -1
- package/dist/lib/session/db.js +230 -1
- package/dist/lib/session/digest.d.ts +1 -1
- package/dist/lib/session/digest.js +1 -1
- package/dist/lib/share/publish.js +24 -0
- package/dist/lib/snapshot.d.ts +103 -0
- package/dist/lib/snapshot.js +99 -0
- package/dist/lib/startup/command-registry.d.ts +1 -1
- package/dist/lib/startup/command-registry.js +2 -2
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/types.d.ts +11 -2
- package/dist/lib/usage.d.ts +5 -0
- package/dist/lib/usage.js +3 -3
- package/package.json +1 -1
- package/dist/commands/activity.d.ts +0 -87
- package/dist/commands/activity.js +0 -346
|
@@ -23,13 +23,13 @@ import { getActiveSessions } from '../lib/session/active.js';
|
|
|
23
23
|
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
24
24
|
import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
25
25
|
import { factoryProjectsPath } from '../lib/auto-dispatch.js';
|
|
26
|
-
import { formatFleetWorkspaces, parseRemoteProbe, probeProjectWorkspaces, workspaceTargetsForDef, } from '../lib/project-probe.js';
|
|
26
|
+
import { formatFleetWorkspaces, parseRemoteProbe, probeProjectWorkspaces, workspaceTargetsForDef, workspaceWarnings, } from '../lib/project-probe.js';
|
|
27
27
|
import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, projectDefPath, isSafeProjectName, } from '../lib/projects.js';
|
|
28
|
-
import { rollupSessionsByProject, isDeadStatus, liveDeadSplit, enrichProjectSignals,
|
|
28
|
+
import { rollupSessionsByProject, withDefaultMachine, isDeadStatus, liveDeadSplit, enrichProjectSignals, formatProjectMembersByHost, formatProjectWarnings, } from '../lib/project-status.js';
|
|
29
29
|
import { fetchLinearProjectCounts } from '../lib/linear-project-counts.js';
|
|
30
30
|
import { listLinearProjects, pickLinearProject } from '../lib/linear-projects.js';
|
|
31
31
|
import { checkRepoSlug } from '../lib/project-doctor.js';
|
|
32
|
-
import { readFocusAreas } from '../lib/project-focus.js';
|
|
32
|
+
import { formatFocusAreas, readFocusAreas } from '../lib/project-focus.js';
|
|
33
33
|
import { formatVerdict, scheduleVerdict } from '../lib/project-schedule.js';
|
|
34
34
|
import { buildFactoryImportCandidates, buildLinearImportCandidates, validateImportOpts, } from '../lib/project-import.js';
|
|
35
35
|
/** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
|
|
@@ -232,7 +232,7 @@ export function formatMilestoneLines(milestones, next, nowMs, limit) {
|
|
|
232
232
|
// an earlier one and hide the actual next behind "+N more", which is the one
|
|
233
233
|
// thing this row exists to say. Identity is name+targetDate: two milestones
|
|
234
234
|
// can share a name, and matching on name alone labelled the wrong row.
|
|
235
|
-
const key = (m) => `${m.name}
|
|
235
|
+
const key = (m) => `${m.name}${m.targetDate ?? ''}`;
|
|
236
236
|
const lead = next ? milestones.filter((m) => key(m) === key(next)).slice(0, 1) : [];
|
|
237
237
|
const others = next ? milestones.filter((m) => key(m) !== key(next)) : milestones;
|
|
238
238
|
const ordered = [...lead, ...others];
|
|
@@ -291,7 +291,12 @@ function statusBar(r) {
|
|
|
291
291
|
* artifact log and git focus, and skips just the network calls (`gh`, Linear).
|
|
292
292
|
*/
|
|
293
293
|
async function enrichProjectsForRender(defs, all, opts) {
|
|
294
|
-
|
|
294
|
+
// Local getActiveSessions() leaves `machine` unset (the sessions renderer
|
|
295
|
+
// falls back to this box). Host-grouped agents need an explicit stamp so
|
|
296
|
+
// under `--fleet` this box does not render as `@local` next to peers that
|
|
297
|
+
// carry real device ids.
|
|
298
|
+
const local = withDefaultMachine(await getActiveSessions(), machineId());
|
|
299
|
+
const roll = rollupSessionsByProject(all, [...local, ...(opts.extraSessions ?? [])]);
|
|
295
300
|
const remote = new Map();
|
|
296
301
|
const linear = new Map();
|
|
297
302
|
// Local git, no API, no rate limit — measured 0.23s over a 897-commit week.
|
|
@@ -317,7 +322,12 @@ milestoneLimit = 1,
|
|
|
317
322
|
/** Directories the window's work landed in, from local git. */
|
|
318
323
|
focus = [],
|
|
319
324
|
/** `view` mode: the caller prints the stored definition in full afterwards. */
|
|
320
|
-
detail = false
|
|
325
|
+
detail = false,
|
|
326
|
+
/**
|
|
327
|
+
* Workspace probe rows used only for the warnings footer. May be the full
|
|
328
|
+
* `--fleet` set or a local-only probe so drift is never silent by default.
|
|
329
|
+
*/
|
|
330
|
+
warnWorkspaces = []) {
|
|
321
331
|
// The headline counts LIVE agents. It used to be every matched session, which
|
|
322
332
|
// read `39 agents` on a project where 19 had crashed. `planPct` used to sit
|
|
323
333
|
// here too and is gone: it summed each session's latest checklist snapshot,
|
|
@@ -330,15 +340,18 @@ detail = false) {
|
|
|
330
340
|
if (split.dead > 0) {
|
|
331
341
|
// Wreckage is worth a number of its own — 19 crashed sessions is a thing to
|
|
332
342
|
// go fix, not a throughput signal to fold into the headline.
|
|
333
|
-
const
|
|
334
|
-
console.log(` ${chalk.dim('dead')} ${chalk.yellow(`${split.dead} finished or lost`)} ${chalk.dim(`(${
|
|
343
|
+
const deadDetail = split.deadByStatus.map((d) => `${d.n} ${d.status}`).join(', ');
|
|
344
|
+
console.log(` ${chalk.dim('dead')} ${chalk.yellow(`${split.dead} finished or lost`)} ${chalk.dim(`(${deadDetail})`)}`);
|
|
335
345
|
}
|
|
336
|
-
// Live only
|
|
337
|
-
//
|
|
338
|
-
// already accounts for, counted twice and contradicting the headline.
|
|
346
|
+
// Live only, grouped by host when machine stamps exist so "who is on which
|
|
347
|
+
// box" is visible. Flat collapse hid that when harness×status matched across hosts.
|
|
339
348
|
const liveMembers = r?.members.filter((m) => !isDeadStatus(m.status)) ?? [];
|
|
340
|
-
if (liveMembers.length)
|
|
341
|
-
|
|
349
|
+
if (liveMembers.length) {
|
|
350
|
+
const agentLines = formatProjectMembersByHost(liveMembers);
|
|
351
|
+
agentLines.forEach((line, i) => {
|
|
352
|
+
console.log(` ${chalk.dim((i === 0 ? 'agents' : '').padEnd(7))} ${line}`);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
342
355
|
const ships = [];
|
|
343
356
|
if (remote?.mergedPrs) {
|
|
344
357
|
ships.push(chalk.green(`${remote.mergedPrs}${remote.mergedPrsTruncated ? '+' : ''} merged (${remote.windowDays}d)`));
|
|
@@ -357,13 +370,15 @@ detail = false) {
|
|
|
357
370
|
for (const line of formatMilestoneLines(linear?.milestones ?? [], linear?.nextMilestone, nowMs, milestoneLimit)) {
|
|
358
371
|
console.log(line);
|
|
359
372
|
}
|
|
360
|
-
//
|
|
373
|
+
// Informational schedule only. Warn-level verdicts land in the footer so the
|
|
374
|
+
// bottom of the card is the one place you look for "what needs attention".
|
|
361
375
|
const verdict = linear?.milestones?.length ? formatVerdict(scheduleVerdict(linear.milestones, nowMs)) : undefined;
|
|
362
|
-
if (verdict) {
|
|
363
|
-
console.log(` ${chalk.dim('schedule')} ${verdict.
|
|
376
|
+
if (verdict && !verdict.warn) {
|
|
377
|
+
console.log(` ${chalk.dim('schedule')} ${verdict.text}`);
|
|
364
378
|
}
|
|
365
379
|
if (focus.length) {
|
|
366
|
-
|
|
380
|
+
const windowDays = remote?.windowDays ?? 7;
|
|
381
|
+
console.log(` ${chalk.dim('focus')} ${formatFocusAreas(focus, windowDays)}`);
|
|
367
382
|
}
|
|
368
383
|
if (r && r.tickets.length) {
|
|
369
384
|
console.log(` ${chalk.dim('tickets')} ${r.tickets.slice(0, 8).join(' · ')}${r.tickets.length > 8 ? ' …' : ''}`);
|
|
@@ -384,14 +399,29 @@ detail = false) {
|
|
|
384
399
|
const repos = [def.repo, ...(def.repos ?? []).map((x) => x.slug)].filter(Boolean);
|
|
385
400
|
if (repos.length)
|
|
386
401
|
console.log(` ${chalk.dim('repos')} ${[...new Set(repos)].join(' · ')}`);
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
402
|
+
// Warnings footer — critical (🔴) then continue (⚠️). Sources: repo slug
|
|
403
|
+
// mismatch, workspace drift/dirty/missing, schedule that cannot measure.
|
|
404
|
+
const warnings = [];
|
|
390
405
|
const mismatch = def.root ? checkRepoSlug(def, originSlug(expandLocalHome(def.root))) : undefined;
|
|
391
406
|
if (mismatch) {
|
|
392
|
-
|
|
393
|
-
|
|
407
|
+
warnings.push({ severity: 'critical', text: mismatch.message, remediation: mismatch.remediation });
|
|
408
|
+
}
|
|
409
|
+
for (const w of workspaceWarnings(warnWorkspaces)) {
|
|
410
|
+
warnings.push(w);
|
|
394
411
|
}
|
|
412
|
+
if (verdict?.warn) {
|
|
413
|
+
warnings.push({ severity: 'continue', text: verdict.text });
|
|
414
|
+
}
|
|
415
|
+
if (split.dead > 0 && (split.deadByStatus.find((d) => d.status === 'crashed')?.n ?? 0) > 0) {
|
|
416
|
+
const crashed = split.deadByStatus.find((d) => d.status === 'crashed').n;
|
|
417
|
+
warnings.push({
|
|
418
|
+
severity: crashed >= 10 ? 'critical' : 'continue',
|
|
419
|
+
text: `${crashed} crashed session${crashed === 1 ? '' : 's'} on this project`,
|
|
420
|
+
remediation: 'inspect with agents sessions --active / clean up stuck worktrees',
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
for (const line of formatProjectWarnings(warnings))
|
|
424
|
+
console.log(line);
|
|
395
425
|
// `view` prints these in full (path + purpose, label + URL) right below, so
|
|
396
426
|
// the compact one-line summaries would just say the same thing twice.
|
|
397
427
|
if (!detail && def.goals?.length) {
|
|
@@ -417,7 +447,8 @@ export function registerProjectsCommands(program) {
|
|
|
417
447
|
agents projects add rush --repo phnx-labs/rush --path apps/web
|
|
418
448
|
agents projects list
|
|
419
449
|
agents projects status # progress card for every project
|
|
420
|
-
agents projects status rush
|
|
450
|
+
agents projects status rush # one project (same body as view/show)
|
|
451
|
+
agents projects view rush # alias of status <name>
|
|
421
452
|
agents projects status --fleet # + per-device workspace drift over SSH
|
|
422
453
|
agents projects link rush --linear # bind the Linear project (auto-suggest)
|
|
423
454
|
agents run --project rush # land an agent in the project
|
|
@@ -522,56 +553,8 @@ export function registerProjectsCommands(program) {
|
|
|
522
553
|
console.log(chalk.gray(` ${target}`));
|
|
523
554
|
console.log(chalk.gray(` root ${def.root}${def.repo ? ` · repo ${def.repo}` : ''}`));
|
|
524
555
|
});
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
.command('view <name>')
|
|
528
|
-
.alias('show')
|
|
529
|
-
.description('Everything about one project: the full status card, every milestone, and the stored definition.')
|
|
530
|
-
.option('--json', 'Machine-readable output')
|
|
531
|
-
.option('--window <days>', 'Window for merged PRs, artifacts, and focus areas', '7')
|
|
532
|
-
.option('--no-remote', 'Skip the GitHub and Linear lookups; definition only')
|
|
533
|
-
.action(async (name, opts) => {
|
|
534
|
-
const def = loadProjectDef(name);
|
|
535
|
-
if (!def) {
|
|
536
|
-
console.error(chalk.red(`No project named "${name}". List them: agents projects list`));
|
|
537
|
-
process.exit(1);
|
|
538
|
-
}
|
|
539
|
-
// `view` is `status` for one project PLUS the stored definition and every
|
|
540
|
-
// milestone. It used to render its own short list — root, repos, a raw
|
|
541
|
-
// Linear id, issues — so opening a single project told you LESS than the
|
|
542
|
-
// roll-up did: no agents, no ships, no focus, no schedule verdict.
|
|
543
|
-
const windowDays = Math.max(1, Number.parseInt(opts.window ?? '7', 10) || 7);
|
|
544
|
-
const nowMs = Date.now();
|
|
545
|
-
const all = listProjectDefs();
|
|
546
|
-
const { roll, remote, linear, focus } = await enrichProjectsForRender([def], all, {
|
|
547
|
-
windowDays,
|
|
548
|
-
nowMs,
|
|
549
|
-
skipRemote: opts.remote === false,
|
|
550
|
-
});
|
|
551
|
-
const r = roll.get(def.name);
|
|
552
|
-
const sig = remote.get(def.name);
|
|
553
|
-
const counts = linear.get(def.name);
|
|
554
|
-
if (opts.json) {
|
|
555
|
-
console.log(JSON.stringify({
|
|
556
|
-
...def,
|
|
557
|
-
linear: { ...def.linear, ...(counts ?? {}) },
|
|
558
|
-
schedule: counts?.milestones?.length ? scheduleVerdict(counts.milestones, nowMs) : null,
|
|
559
|
-
live: r ? liveDeadSplit(r.byStatus).live : 0,
|
|
560
|
-
dead: r ? liveDeadSplit(r.byStatus).dead : 0,
|
|
561
|
-
byStatus: r?.byStatus ?? {},
|
|
562
|
-
members: r?.members ?? [],
|
|
563
|
-
openPrs: r?.openPrs ?? [],
|
|
564
|
-
tickets: r?.tickets ?? [],
|
|
565
|
-
mergedPrs: sig?.mergedPrs ?? 0,
|
|
566
|
-
latestRelease: sig?.latestRelease ?? null,
|
|
567
|
-
artifacts: sig?.artifacts ?? 0,
|
|
568
|
-
focus: focus.get(def.name) ?? [],
|
|
569
|
-
}, null, 2));
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
// The same card `status` prints, with every milestone instead of the next.
|
|
573
|
-
renderCard(def, r, sig, undefined, counts, nowMs, Number.POSITIVE_INFINITY, focus.get(def.name) ?? [], true);
|
|
574
|
-
// Then what only `view` shows: the stored definition, in full.
|
|
556
|
+
/** Print the YAML-side fields that sit under the shared card in `view` mode. */
|
|
557
|
+
function printProjectDefinition(def, name) {
|
|
575
558
|
console.log();
|
|
576
559
|
if (def.root)
|
|
577
560
|
console.log(` ${chalk.dim('root')} ${def.root}`);
|
|
@@ -593,38 +576,17 @@ export function registerProjectsCommands(program) {
|
|
|
593
576
|
for (const d of def.docs ?? [])
|
|
594
577
|
console.log(` ${chalk.dim('doc')} ${d}`);
|
|
595
578
|
console.log(chalk.gray(` ${projectDefPath(name)}`));
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
.command('edit <name>')
|
|
600
|
-
.description('Open the project YAML in $EDITOR (it is hand-editable regardless).')
|
|
601
|
-
.action((name) => {
|
|
602
|
-
const target = projectDefPath(name);
|
|
603
|
-
if (!fs.existsSync(target)) {
|
|
604
|
-
console.error(chalk.red(`No project named "${name}". Create it: agents projects add ${name}`));
|
|
605
|
-
process.exit(1);
|
|
606
|
-
}
|
|
607
|
-
const editor = process.env.VISUAL || process.env.EDITOR || 'vi';
|
|
608
|
-
// $EDITOR commonly carries args ("code --wait") — split like monitors/routines do.
|
|
609
|
-
const parts = editor.split(/\s+/).filter(Boolean);
|
|
610
|
-
const res = spawnSync(parts[0], [...parts.slice(1), target], { stdio: 'inherit' });
|
|
611
|
-
process.exit(res.status ?? 0);
|
|
612
|
-
});
|
|
613
|
-
// ---- status ----
|
|
614
|
-
projects
|
|
615
|
-
.command('status [name]')
|
|
616
|
-
.description('Progress rollup: agents, plan %, merged/open PRs, tickets, and artifacts per project.')
|
|
617
|
-
.option('--json', 'Machine-readable output')
|
|
618
|
-
.option('--window <days>', 'Window for merged PRs and artifacts', '7')
|
|
619
|
-
.option('--no-remote', 'Skip the GitHub lookup (merged-PR count); faster, offline')
|
|
620
|
-
.option('--fleet', 'Also dial every fleet device for workspace presence, branch, and drift (one SSH per peer)')
|
|
621
|
-
.action(async (name, opts) => {
|
|
579
|
+
}
|
|
580
|
+
async function runProjectCard(name, opts, mode) {
|
|
581
|
+
const detail = mode === 'view';
|
|
622
582
|
const all = listProjectDefs();
|
|
623
583
|
// Named lookup goes through the strict single-def loader so a broken
|
|
624
584
|
// <name>.yaml surfaces its validation error instead of "No project named".
|
|
625
585
|
const defs = name ? [loadProjectDef(name)].filter((d) => d !== undefined) : all;
|
|
626
586
|
if (name && !defs.length) {
|
|
627
|
-
console.error(chalk.red(
|
|
587
|
+
console.error(chalk.red(detail
|
|
588
|
+
? `No project named "${name}". List them: agents projects list`
|
|
589
|
+
: `No project named "${name}".`));
|
|
628
590
|
process.exit(1);
|
|
629
591
|
}
|
|
630
592
|
if (!defs.length) {
|
|
@@ -639,7 +601,8 @@ export function registerProjectsCommands(program) {
|
|
|
639
601
|
// --fleet: probe each shown def's workspace paths (root + repos[].path)
|
|
640
602
|
// locally and on every peer in one parallel SSH round, and widen the
|
|
641
603
|
// live-session rollup to the whole fleet via the existing sessions
|
|
642
|
-
// fan-out. Both are opt-in — they dial the fleet.
|
|
604
|
+
// fan-out. Both are opt-in — they dial the fleet. `view` accepts the flag
|
|
605
|
+
// too so the two verbs stay interchangeable once a name is given.
|
|
643
606
|
const fleetTargets = opts.fleet ? [...new Set(defs.flatMap(workspaceTargetsForDef))] : [];
|
|
644
607
|
let fleetWs = [];
|
|
645
608
|
let fleetSkipped = [];
|
|
@@ -673,28 +636,41 @@ export function registerProjectsCommands(program) {
|
|
|
673
636
|
const targets = new Set(workspaceTargetsForDef(d));
|
|
674
637
|
return fleetWs.filter((s) => targets.has(s.path));
|
|
675
638
|
};
|
|
639
|
+
/** Local-only workspace probe for the warnings footer when --fleet is off. */
|
|
640
|
+
const localWsCache = new Map();
|
|
641
|
+
const localWsFor = (d) => {
|
|
642
|
+
const cached = localWsCache.get(d.name);
|
|
643
|
+
if (cached)
|
|
644
|
+
return cached;
|
|
645
|
+
const self = machineId();
|
|
646
|
+
const rows = probeProjectWorkspaces(workspaceTargetsForDef(d)).map((s) => ({ ...s, host: self }));
|
|
647
|
+
localWsCache.set(d.name, rows);
|
|
648
|
+
return rows;
|
|
649
|
+
};
|
|
676
650
|
if (opts.json) {
|
|
677
651
|
if (fleetSkipped.length > 0)
|
|
678
652
|
process.stderr.write(formatFleetSkippedNote(fleetSkipped));
|
|
653
|
+
// `view` used to nest the def fields at the top level and omit plan /
|
|
654
|
+
// worktrees / windowDays; keep one machine shape for both verbs so a
|
|
655
|
+
// consumer that switches `status`↔`view` does not re-learn the schema.
|
|
679
656
|
console.log(JSON.stringify(defs.map((d) => {
|
|
680
657
|
const r = roll.get(d.name);
|
|
681
658
|
const rem = remote.get(d.name);
|
|
659
|
+
const counts = linear.get(d.name);
|
|
682
660
|
return {
|
|
683
|
-
name: d.name,
|
|
661
|
+
...(detail ? d : { name: d.name }),
|
|
684
662
|
agents: r?.agents ?? 0,
|
|
685
663
|
byStatus: r?.byStatus ?? {},
|
|
686
664
|
members: r?.members ?? [],
|
|
687
665
|
plan: r?.plan ?? { done: 0, total: 0 },
|
|
688
|
-
schedule:
|
|
689
|
-
? scheduleVerdict(linear.get(d.name).milestones, nowMs)
|
|
690
|
-
: null,
|
|
666
|
+
schedule: counts?.milestones?.length ? scheduleVerdict(counts.milestones, nowMs) : null,
|
|
691
667
|
focus: focus.get(d.name) ?? [],
|
|
692
668
|
live: r ? liveDeadSplit(r.byStatus).live : 0,
|
|
693
669
|
dead: r ? liveDeadSplit(r.byStatus).dead : 0,
|
|
694
670
|
openPrs: r?.openPrs ?? [],
|
|
695
671
|
mergedPrs: rem?.mergedPrs ?? 0,
|
|
696
672
|
latestRelease: rem?.latestRelease ?? null,
|
|
697
|
-
linear: linear
|
|
673
|
+
linear: detail ? { ...d.linear, ...(counts ?? {}) } : (counts ?? null),
|
|
698
674
|
tickets: r?.tickets ?? [],
|
|
699
675
|
worktrees: r?.worktrees ?? 0,
|
|
700
676
|
artifacts: rem?.artifacts ?? 0,
|
|
@@ -706,11 +682,50 @@ export function registerProjectsCommands(program) {
|
|
|
706
682
|
}), null, 2));
|
|
707
683
|
return;
|
|
708
684
|
}
|
|
685
|
+
// Compact rollup shows the next milestone; `view` shows every declared one.
|
|
686
|
+
const milestoneLimit = detail ? Number.POSITIVE_INFINITY : 1;
|
|
709
687
|
for (const d of defs) {
|
|
710
|
-
renderCard(d, roll.get(d.name), remote.get(d.name), opts.fleet ? fleetFor(d) : undefined, linear.get(d.name), nowMs,
|
|
688
|
+
renderCard(d, roll.get(d.name), remote.get(d.name), opts.fleet ? fleetFor(d) : undefined, linear.get(d.name), nowMs, milestoneLimit, focus.get(d.name) ?? [], detail,
|
|
689
|
+
// Always feed workspace rows into the warnings footer — full fleet when
|
|
690
|
+
// --fleet, otherwise a local-only probe so behind/dirty is never silent.
|
|
691
|
+
opts.fleet ? fleetFor(d) : localWsFor(d));
|
|
692
|
+
if (detail)
|
|
693
|
+
printProjectDefinition(d, d.name);
|
|
711
694
|
}
|
|
712
695
|
if (fleetSkipped.length > 0)
|
|
713
696
|
process.stdout.write(formatFleetSkippedNote(fleetSkipped));
|
|
697
|
+
}
|
|
698
|
+
projects
|
|
699
|
+
.command('status [name]')
|
|
700
|
+
.alias('view')
|
|
701
|
+
.alias('show')
|
|
702
|
+
.description('Progress card for every project, or one named project (aliases: view, show). Named form also prints every milestone and the stored definition.')
|
|
703
|
+
.option('--json', 'Machine-readable output')
|
|
704
|
+
.option('--window <days>', 'Window for merged PRs, artifacts, and focus areas', '7')
|
|
705
|
+
.option('--no-remote', 'Skip the GitHub and Linear lookups; faster, offline')
|
|
706
|
+
.option('--fleet', 'Also dial every fleet device for workspace presence, branch, and drift (one SSH per peer)')
|
|
707
|
+
.action(async (name, opts) => {
|
|
708
|
+
// Named invocation = `view` depth (all milestones + definition). Unnamed
|
|
709
|
+
// stays the scannable multi-project rollup. `view`/`show` are commander
|
|
710
|
+
// aliases of this same command, so there is only one implementation.
|
|
711
|
+
const mode = name ? 'view' : 'status';
|
|
712
|
+
await runProjectCard(name, opts, mode);
|
|
713
|
+
});
|
|
714
|
+
// ---- edit ----
|
|
715
|
+
projects
|
|
716
|
+
.command('edit <name>')
|
|
717
|
+
.description('Open the project YAML in $EDITOR (it is hand-editable regardless).')
|
|
718
|
+
.action((name) => {
|
|
719
|
+
const target = projectDefPath(name);
|
|
720
|
+
if (!fs.existsSync(target)) {
|
|
721
|
+
console.error(chalk.red(`No project named "${name}". Create it: agents projects add ${name}`));
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
const editor = process.env.VISUAL || process.env.EDITOR || 'vi';
|
|
725
|
+
// $EDITOR commonly carries args ("code --wait") — split like monitors/routines do.
|
|
726
|
+
const parts = editor.split(/\s+/).filter(Boolean);
|
|
727
|
+
const res = spawnSync(parts[0], [...parts.slice(1), target], { stdio: 'inherit' });
|
|
728
|
+
process.exit(res.status ?? 0);
|
|
714
729
|
});
|
|
715
730
|
// ---- probe (hidden; the peer half of `status --fleet`) ----
|
|
716
731
|
projects
|
|
@@ -628,8 +628,8 @@ export function registerRoutinesCommands(program) {
|
|
|
628
628
|
.option('-t, --timeout <timeout>', 'Kill the agent if it runs longer than this (e.g., 10m, 2h, 3d, 1w; max 1w)', '10m')
|
|
629
629
|
.option('--timezone <tz>', 'Interpret schedule in this timezone (e.g., America/Los_Angeles)')
|
|
630
630
|
.option('--devices <names>', 'Fleet allowlist (comma-separated): only listed devices schedule and fire this routine. Omit for unrestricted.')
|
|
631
|
-
.option('--run-on <name>', '
|
|
632
|
-
.option('--placement <strategy>', `
|
|
631
|
+
.option('--run-on <name>', 'BODY placement: execute the job body on this machine over SSH (registered host, device, capability tag, or user@host). Sets hostStrategy=host. Not eligibility — see --devices. Auto-pins devices to THIS machine unless --devices is given. Same model as agents run --where device:<name> (docs/00-concepts.md#placement).')
|
|
632
|
+
.option('--placement <strategy>', `BODY placement strategy: ${HOST_STRATEGIES.join('|')} (default: local, or host when --run-on is set). Maps to the shared Placement model (local|device|fleet|cloud). Not the same as --host (which manages routines on a remote machine).`)
|
|
633
633
|
.option('--run-cwd <dir>', 'Working directory on the --run-on host (--remote-cwd is taken by the remote-management passthrough)')
|
|
634
634
|
.option('--at <time>', 'One-shot mode: run once at this time (e.g., "14:30" or "2026-02-24 09:00"), then disable')
|
|
635
635
|
.option('--on <source:event>', 'Webhook trigger instead of/in addition to a schedule: github:pull_request or linear:Issue')
|
|
@@ -36,6 +36,8 @@ function disabledReason(candidate) {
|
|
|
36
36
|
return undefined;
|
|
37
37
|
if (readiness.reason === 'signed_out')
|
|
38
38
|
return 'logged out';
|
|
39
|
+
if (readiness.reason === 'revoked')
|
|
40
|
+
return 'needs re-login';
|
|
39
41
|
if (readiness.reason === 'out_of_credits')
|
|
40
42
|
return 'out of credits';
|
|
41
43
|
const windows = candidate.usageSnapshot?.windows ?? [];
|
package/dist/commands/secrets.js
CHANGED
|
@@ -2071,7 +2071,7 @@ Examples:
|
|
|
2071
2071
|
.action(async (bundleName, opts) => {
|
|
2072
2072
|
try {
|
|
2073
2073
|
// `--device` is an alias for `--host` (fleet vocabulary parity with
|
|
2074
|
-
// `agents
|
|
2074
|
+
// `agents run --device`); fold it into the host list up front so
|
|
2075
2075
|
// every downstream branch sees one resolved target list.
|
|
2076
2076
|
if (opts.device?.length)
|
|
2077
2077
|
opts.host = [...(opts.host ?? []), ...opts.device];
|
|
@@ -28,5 +28,38 @@ interface ToolBackfillOptions {
|
|
|
28
28
|
}
|
|
29
29
|
export declare function backfillToolsLocal(options: ToolBackfillOptions, oneBatch?: boolean): Promise<ToolBackfillMachineResult>;
|
|
30
30
|
export declare function runToolsBackfill(options: ToolBackfillOptions): Promise<ToolBackfillEnvelope>;
|
|
31
|
+
interface ResourceBackfillOptions {
|
|
32
|
+
agent?: string;
|
|
33
|
+
project?: string;
|
|
34
|
+
since?: string;
|
|
35
|
+
until?: string;
|
|
36
|
+
teams?: boolean;
|
|
37
|
+
json?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface ResourceBackfillEnvelope {
|
|
40
|
+
schemaVersion: 1;
|
|
41
|
+
kind: 'resources-backfill';
|
|
42
|
+
generatedAt: string;
|
|
43
|
+
machine: string;
|
|
44
|
+
/** Sessions considered (matched the filter, had a real transcript). */
|
|
45
|
+
scanned: number;
|
|
46
|
+
/** Sessions (re)parsed and written this run. */
|
|
47
|
+
updated: number;
|
|
48
|
+
/** Sessions already current at this extractor version, skipped. */
|
|
49
|
+
skipped: number;
|
|
50
|
+
/** Sessions whose transcript could not be stat'd or parsed. */
|
|
51
|
+
failed: number;
|
|
52
|
+
/** Total session_resource_usage rows written across updated sessions. */
|
|
53
|
+
resourceRows: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Populate session_resource_usage for historical sessions on THIS machine.
|
|
57
|
+
* Local-only by design: the resource signal is derived from each machine's own
|
|
58
|
+
* transcripts, and the index is machine-local, so there is no cross-machine
|
|
59
|
+
* merge to do — run it on each box (or over `agents ssh`). Ensures the session
|
|
60
|
+
* index is complete first (discoverSessions), then re-derives usage gated by the
|
|
61
|
+
* resource_scan_ledger so reruns skip completed transcripts.
|
|
62
|
+
*/
|
|
63
|
+
export declare function runResourceBackfill(options: ResourceBackfillOptions): Promise<ResourceBackfillEnvelope>;
|
|
31
64
|
export declare function registerSessionsBackfillCommand(sessionsCmd: Command): void;
|
|
32
65
|
export {};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { setHelpSections } from '../lib/help.js';
|
|
3
3
|
import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
4
|
-
import { discoverSessions } from '../lib/session/discover.js';
|
|
4
|
+
import { discoverSessions, parseTimeFilter } from '../lib/session/discover.js';
|
|
5
5
|
import { machineId } from '../lib/session/sync/config.js';
|
|
6
6
|
import { SESSION_AGENTS } from '../lib/session/types.js';
|
|
7
7
|
import { ensureToolIndex, readToolIndexCoverage } from '../lib/session/tool-index.js';
|
|
8
8
|
import { NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
9
|
+
import { backfillResourceUsage } from '../lib/session/db.js';
|
|
9
10
|
const BACKFILL_REMOTE_TIMEOUT_MS = 10 * 60_000;
|
|
10
11
|
function parseAgent(value) {
|
|
11
12
|
if (!value)
|
|
@@ -138,6 +139,48 @@ export async function runToolsBackfill(options) {
|
|
|
138
139
|
machines,
|
|
139
140
|
};
|
|
140
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Populate session_resource_usage for historical sessions on THIS machine.
|
|
144
|
+
* Local-only by design: the resource signal is derived from each machine's own
|
|
145
|
+
* transcripts, and the index is machine-local, so there is no cross-machine
|
|
146
|
+
* merge to do — run it on each box (or over `agents ssh`). Ensures the session
|
|
147
|
+
* index is complete first (discoverSessions), then re-derives usage gated by the
|
|
148
|
+
* resource_scan_ledger so reruns skip completed transcripts.
|
|
149
|
+
*/
|
|
150
|
+
export async function runResourceBackfill(options) {
|
|
151
|
+
const { agent, version } = parseAgent(options.agent);
|
|
152
|
+
// Make sure every matching transcript is indexed before we re-derive usage.
|
|
153
|
+
await discoverSessions({
|
|
154
|
+
agent,
|
|
155
|
+
version,
|
|
156
|
+
project: options.project,
|
|
157
|
+
since: options.since,
|
|
158
|
+
until: options.until,
|
|
159
|
+
all: true,
|
|
160
|
+
unbounded: true,
|
|
161
|
+
skipExistenceCheck: true,
|
|
162
|
+
excludeTeamOrigin: !options.teams,
|
|
163
|
+
});
|
|
164
|
+
const filter = { excludeTeamOrigin: !options.teams };
|
|
165
|
+
if (agent)
|
|
166
|
+
filter.agent = agent;
|
|
167
|
+
if (version)
|
|
168
|
+
filter.version = version;
|
|
169
|
+
if (options.project)
|
|
170
|
+
filter.project = options.project;
|
|
171
|
+
if (options.since)
|
|
172
|
+
filter.sinceMs = parseTimeFilter(options.since);
|
|
173
|
+
if (options.until)
|
|
174
|
+
filter.untilMs = parseTimeFilter(options.until);
|
|
175
|
+
const result = backfillResourceUsage(filter);
|
|
176
|
+
return {
|
|
177
|
+
schemaVersion: 1,
|
|
178
|
+
kind: 'resources-backfill',
|
|
179
|
+
generatedAt: new Date().toISOString(),
|
|
180
|
+
machine: machineId(),
|
|
181
|
+
...result,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
141
184
|
export function registerSessionsBackfillCommand(sessionsCmd) {
|
|
142
185
|
const backfill = sessionsCmd.command('backfill').description('Populate derived session data explicitly.');
|
|
143
186
|
const tools = backfill.command('tools').description('Parse historical tool calls once into the local SQLite index.');
|
|
@@ -183,4 +226,43 @@ export function registerSessionsBackfillCommand(sessionsCmd) {
|
|
|
183
226
|
process.exitCode = 1;
|
|
184
227
|
}
|
|
185
228
|
});
|
|
229
|
+
const resources = backfill.command('resources').description('Derive historical skill/slash-command usage once into the local SQLite index.');
|
|
230
|
+
setHelpSections(resources, {
|
|
231
|
+
examples: `
|
|
232
|
+
# Fold every historical session on this machine into the usage index
|
|
233
|
+
agents sessions backfill resources
|
|
234
|
+
|
|
235
|
+
# Narrow the historical work
|
|
236
|
+
agents sessions backfill resources --agent claude --since 30d
|
|
237
|
+
|
|
238
|
+
# Machine-readable summary
|
|
239
|
+
agents sessions backfill resources --json
|
|
240
|
+
`,
|
|
241
|
+
notes: `
|
|
242
|
+
- Populates session_resource_usage for sessions indexed before the usage signal shipped. New/changed sessions are recorded on their normal scan; this is the one-shot catch-up read by \`agents sessions stats\`.
|
|
243
|
+
- Local-only: the signal is derived per machine from its own transcripts. Run it on each box (or over \`agents ssh <host> agents sessions backfill resources\`).
|
|
244
|
+
- Reruns skip transcripts already current (resource_scan_ledger); bump the extractor version to force a full re-derive.
|
|
245
|
+
- Only slash commands and \`Skill\` tool calls are recorded — auto-triggered skills emit no signal.
|
|
246
|
+
`,
|
|
247
|
+
});
|
|
248
|
+
resources.action(async (_options, command) => {
|
|
249
|
+
const options = command.optsWithGlobals();
|
|
250
|
+
try {
|
|
251
|
+
const envelope = await runResourceBackfill(options);
|
|
252
|
+
if (options.json)
|
|
253
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
|
|
254
|
+
else {
|
|
255
|
+
console.log(`${envelope.machine}: derived usage for ${envelope.updated.toLocaleString()} session${envelope.updated === 1 ? '' : 's'} ` +
|
|
256
|
+
`(${envelope.resourceRows.toLocaleString()} resource row${envelope.resourceRows === 1 ? '' : 's'}); ` +
|
|
257
|
+
`${envelope.skipped.toLocaleString()} already current, ${envelope.scanned.toLocaleString()} scanned.`);
|
|
258
|
+
if (envelope.failed > 0) {
|
|
259
|
+
console.log(chalk.yellow(`${envelope.failed.toLocaleString()} transcript${envelope.failed === 1 ? '' : 's'} could not be parsed; rerun to retry.`));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
186
268
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions stats` — precomputed resource-usage insights.
|
|
3
|
+
*
|
|
4
|
+
* The read side of #12: which skills and slash-commands do you actually invoke,
|
|
5
|
+
* and which installed ones are dead weight? The write side already exists —
|
|
6
|
+
* every scanned transcript's skill/`Skill`-tool + slash-command tallies land in
|
|
7
|
+
* `session_resource_usage` at index time — so this is a cheap SQLite rollup, no
|
|
8
|
+
* re-scan of history. A both-ends view: the most-invoked resources, and the
|
|
9
|
+
* installed-but-never-invoked ones.
|
|
10
|
+
*
|
|
11
|
+
* Signal caveat, surfaced in help and output: only EXPLICIT invocations count
|
|
12
|
+
* (slash commands + `Skill` tool calls). An auto-triggered skill (loaded by
|
|
13
|
+
* description match) emits no event and reads as zero — "0" means "never
|
|
14
|
+
* explicitly invoked", not "never loaded". Skill invocations are recorded for
|
|
15
|
+
* Claude and Kimi (the `Skill`-tool harnesses); slash-commands are Claude-only;
|
|
16
|
+
* other harnesses contribute nothing.
|
|
17
|
+
*/
|
|
18
|
+
import type { Command } from 'commander';
|
|
19
|
+
/** One installed resource, keyed the same way session_resource_usage stores it. */
|
|
20
|
+
export interface InstalledResource {
|
|
21
|
+
kind: 'skill' | 'command';
|
|
22
|
+
name: string;
|
|
23
|
+
plugin: string | null;
|
|
24
|
+
source: string | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The both-ends "dead weight" set: installed resources whose identity
|
|
28
|
+
* (kind + name — name already embeds `plugin:short`, so it matches the stored
|
|
29
|
+
* invoked name) never appears in the invoked rows. Sorted kind then name.
|
|
30
|
+
* Pure set-difference so it is unit-testable without touching the filesystem.
|
|
31
|
+
*/
|
|
32
|
+
export declare function diffZeroInvoked(installed: InstalledResource[], invoked: Array<{
|
|
33
|
+
kind: string;
|
|
34
|
+
name: string;
|
|
35
|
+
}>): InstalledResource[];
|
|
36
|
+
export declare function registerSessionsStatsCommand(sessionsCmd: Command): void;
|