@phnx-labs/agents-cli 1.20.51 → 1.20.53
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 +71 -0
- package/README.md +12 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +215 -7
- package/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.js +8 -1
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +2 -3
- package/dist/commands/exec.js +82 -3
- package/dist/commands/factory.js +8 -0
- package/dist/commands/feed.d.ts +38 -0
- package/dist/commands/feed.js +274 -0
- package/dist/commands/logs.d.ts +5 -1
- package/dist/commands/logs.js +248 -3
- package/dist/commands/mcp.js +7 -0
- package/dist/commands/memory.d.ts +9 -0
- package/dist/commands/memory.js +164 -0
- package/dist/commands/message.d.ts +11 -6
- package/dist/commands/message.js +140 -5
- package/dist/commands/routines.js +12 -0
- package/dist/commands/secrets-migrate.d.ts +2 -1
- package/dist/commands/secrets-migrate.js +88 -13
- package/dist/commands/secrets.d.ts +22 -0
- package/dist/commands/secrets.js +176 -42
- package/dist/commands/sessions.js +10 -1
- package/dist/commands/teams.js +4 -0
- package/dist/commands/worktree.js +4 -2
- package/dist/index.js +21 -22
- package/dist/lib/agents.js +249 -17
- package/dist/lib/answer-router.d.ts +75 -0
- package/dist/lib/answer-router.js +149 -0
- package/dist/lib/ask-classifier.d.ts +71 -0
- package/dist/lib/ask-classifier.js +197 -0
- package/dist/lib/browser/login-detection.d.ts +94 -0
- package/dist/lib/browser/login-detection.js +274 -0
- package/dist/lib/browser/profiles.d.ts +17 -8
- package/dist/lib/browser/profiles.js +27 -8
- package/dist/lib/browser/secret-ref.d.ts +10 -0
- package/dist/lib/browser/secret-ref.js +14 -0
- package/dist/lib/browser/service.js +14 -12
- package/dist/lib/cloud/antigravity.d.ts +0 -2
- package/dist/lib/cloud/antigravity.js +2 -17
- package/dist/lib/cloud/codex.js +3 -18
- package/dist/lib/cloud/rush.d.ts +15 -0
- package/dist/lib/cloud/rush.js +10 -16
- package/dist/lib/cloud/stream.js +2 -0
- package/dist/lib/cloud/types.d.ts +21 -0
- package/dist/lib/cloud/types.js +81 -0
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +12 -2
- package/dist/lib/crabbox/lease.d.ts +19 -0
- package/dist/lib/crabbox/lease.js +22 -11
- package/dist/lib/crabbox/progress.d.ts +62 -0
- package/dist/lib/crabbox/progress.js +129 -0
- package/dist/lib/crabbox/runtimes.d.ts +38 -1
- package/dist/lib/crabbox/runtimes.js +98 -5
- package/dist/lib/daemon.d.ts +12 -9
- package/dist/lib/daemon.js +32 -17
- package/dist/lib/events.d.ts +31 -5
- package/dist/lib/events.js +291 -101
- package/dist/lib/exec.js +20 -1
- package/dist/lib/feed-outcome.d.ts +101 -0
- package/dist/lib/feed-outcome.js +244 -0
- package/dist/lib/feed-policy.d.ts +30 -0
- package/dist/lib/feed-policy.js +133 -0
- package/dist/lib/feed.d.ts +180 -0
- package/dist/lib/feed.js +627 -0
- package/dist/lib/git.d.ts +17 -1
- package/dist/lib/git.js +20 -1
- package/dist/lib/hooks.js +529 -14
- package/dist/lib/hosts/passthrough.d.ts +3 -3
- package/dist/lib/hosts/passthrough.js +3 -3
- package/dist/lib/mailbox-gc.d.ts +22 -0
- package/dist/lib/mailbox-gc.js +161 -0
- package/dist/lib/mailbox.d.ts +26 -2
- package/dist/lib/mailbox.js +80 -5
- package/dist/lib/mcp.js +82 -0
- package/dist/lib/memory.d.ts +55 -0
- package/dist/lib/memory.js +274 -0
- package/dist/lib/notify.d.ts +16 -0
- package/dist/lib/notify.js +61 -0
- package/dist/lib/operator.d.ts +26 -0
- package/dist/lib/operator.js +107 -0
- package/dist/lib/plugins.d.ts +35 -0
- package/dist/lib/plugins.js +217 -0
- package/dist/lib/remote-agents-json.d.ts +14 -0
- package/dist/lib/remote-agents-json.js +94 -0
- package/dist/lib/resources/mcp.js +44 -0
- package/dist/lib/resources/memory.d.ts +15 -0
- package/dist/lib/resources/memory.js +46 -0
- package/dist/lib/resources/types.d.ts +2 -2
- package/dist/lib/rotate.js +2 -0
- package/dist/lib/runner.d.ts +43 -0
- package/dist/lib/runner.js +323 -74
- package/dist/lib/sandbox.js +6 -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/agent.d.ts +21 -0
- package/dist/lib/secrets/agent.js +63 -1
- package/dist/lib/secrets/bundles.d.ts +33 -1
- package/dist/lib/secrets/bundles.js +76 -21
- package/dist/lib/secrets/icloud-import.d.ts +79 -0
- package/dist/lib/secrets/icloud-import.js +203 -0
- package/dist/lib/secrets/index.d.ts +142 -2
- package/dist/lib/secrets/index.js +695 -30
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/secrets/sync.js +1 -1
- package/dist/lib/session/active.d.ts +18 -0
- package/dist/lib/session/active.js +47 -17
- package/dist/lib/session/db.d.ts +9 -1
- package/dist/lib/session/db.js +18 -3
- package/dist/lib/session/discover.d.ts +13 -0
- package/dist/lib/session/discover.js +32 -2
- package/dist/lib/session/parse.d.ts +8 -0
- package/dist/lib/session/parse.js +42 -21
- package/dist/lib/session/remote-active.js +8 -89
- package/dist/lib/session/state.d.ts +11 -0
- package/dist/lib/session/state.js +50 -1
- package/dist/lib/session/tail.d.ts +23 -4
- package/dist/lib/session/tail.js +34 -16
- package/dist/lib/session/throughput.d.ts +30 -0
- package/dist/lib/session/throughput.js +86 -0
- package/dist/lib/shim-heal.d.ts +12 -3
- package/dist/lib/shim-heal.js +12 -6
- package/dist/lib/staleness/detectors/subagents.js +57 -3
- package/dist/lib/staleness/writers/hooks.js +7 -3
- package/dist/lib/staleness/writers/subagents.js +37 -6
- 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 +2 -0
- package/dist/lib/state.js +25 -8
- package/dist/lib/subagents.d.ts +52 -0
- package/dist/lib/subagents.js +315 -12
- package/dist/lib/teams/agents.js +6 -3
- package/dist/lib/teams/worktree.d.ts +8 -0
- package/dist/lib/teams/worktree.js +8 -0
- package/dist/lib/types.d.ts +20 -2
- package/dist/lib/versions.js +38 -48
- package/dist/lib/whats-new.d.ts +5 -3
- package/dist/lib/whats-new.js +25 -5
- package/package.json +4 -3
- package/scripts/postinstall.js +61 -1
package/dist/commands/exec.js
CHANGED
|
@@ -327,7 +327,7 @@ export function registerRunCommand(program) {
|
|
|
327
327
|
process.exit(1);
|
|
328
328
|
}
|
|
329
329
|
const backend = typeof options.lease === 'string' ? options.lease : undefined;
|
|
330
|
-
const { detectSignedInRuntimes, pickRuntimes } = await import('../lib/crabbox/runtimes.js');
|
|
330
|
+
const { detectSignedInRuntimes, pickRuntimes, resolveClaudeCredentialsBlob } = await import('../lib/crabbox/runtimes.js');
|
|
331
331
|
const { leaseAndRun } = await import('../lib/crabbox/lease.js');
|
|
332
332
|
const { confirm } = await import('@inquirer/prompts');
|
|
333
333
|
const detected = await detectSignedInRuntimes();
|
|
@@ -344,30 +344,109 @@ export function registerRunCommand(program) {
|
|
|
344
344
|
return `${d?.label ?? id}${d?.email ? ` (${d.email})` : ''}`;
|
|
345
345
|
})
|
|
346
346
|
.join(', ');
|
|
347
|
+
// Claude ships its OAuth token (not just the .claude.json config) — name it
|
|
348
|
+
// explicitly so the consent covers the actual credential transferred.
|
|
349
|
+
const whatShips = runtimes.includes('claude') ? 'credentials + Claude OAuth token' : 'credentials';
|
|
347
350
|
const ok = await confirm({
|
|
348
|
-
message: `Copy
|
|
351
|
+
message: `Copy ${whatShips} for ${names} to a disposable cloud box, run there, then destroy it?`,
|
|
349
352
|
default: false,
|
|
350
353
|
});
|
|
351
354
|
if (!ok) {
|
|
352
355
|
console.error(chalk.yellow('Aborted — no credentials pushed, no box leased.'));
|
|
353
356
|
process.exit(1);
|
|
354
357
|
}
|
|
358
|
+
// Read the Claude OAuth token from the local Keychain (silent) so it can be
|
|
359
|
+
// written to ~/.claude/.credentials.json on the box — otherwise Claude boots
|
|
360
|
+
// "Not logged in". Consent above already covered this transfer.
|
|
361
|
+
let claudeCredentialsJson = null;
|
|
362
|
+
if (runtimes.includes('claude')) {
|
|
363
|
+
const claudeEmail = detected.find((d) => d.id === 'claude')?.email ?? null;
|
|
364
|
+
claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: claudeEmail });
|
|
365
|
+
if (!claudeCredentialsJson) {
|
|
366
|
+
console.error(chalk.yellow('Warning: could not read the local Claude OAuth token — the box may come up "Not logged in".'));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Progress UI. A self-throttled spinner (NOT ora — see progress.ts) covers
|
|
370
|
+
// the otherwise-silent provisioning + box-setup phases; the agent's own
|
|
371
|
+
// output then prints verbatim. The router splits the crabbox stream at the
|
|
372
|
+
// box-side marker. Rule: only ONE spinner phase is active at a time, and no
|
|
373
|
+
// other output is written to stderr while it spins — so it can never storm.
|
|
374
|
+
const { createLeaseOutputRouter, createSpinner } = await import('../lib/crabbox/progress.js');
|
|
375
|
+
const agentName = agentSpec.split('@')[0];
|
|
376
|
+
const spinner = createSpinner({ stream: process.stderr });
|
|
377
|
+
let warmupTimer;
|
|
378
|
+
const stopTimer = () => { if (warmupTimer) {
|
|
379
|
+
clearInterval(warmupTimer);
|
|
380
|
+
warmupTimer = undefined;
|
|
381
|
+
} };
|
|
382
|
+
const fit = (s) => {
|
|
383
|
+
const w = Math.max(20, (process.stderr.columns || 80) - 24);
|
|
384
|
+
return s.length > w ? s.slice(0, w - 1) + '…' : s;
|
|
385
|
+
};
|
|
386
|
+
const router = createLeaseOutputRouter({
|
|
387
|
+
// Setup lines only ever update spinner text (no direct stderr writes),
|
|
388
|
+
// so the spinner stays the single writer during the setup phase.
|
|
389
|
+
onSetupLine: (line) => spinner.update(`Setting up box — ${fit(line)}`),
|
|
390
|
+
onAgentChunk: (chunk) => {
|
|
391
|
+
// First agent byte: stop the setup spinner, THEN stream to stdout.
|
|
392
|
+
if (spinner.active)
|
|
393
|
+
spinner.stopAndPersist('✔', chalk.gray(`Box provisioned — ${agentName} output:`));
|
|
394
|
+
process.stdout.write(chunk);
|
|
395
|
+
},
|
|
396
|
+
});
|
|
355
397
|
try {
|
|
356
398
|
const { exitCode, box, toreDown } = await leaseAndRun({
|
|
357
|
-
agent:
|
|
399
|
+
agent: agentName,
|
|
358
400
|
prompt,
|
|
359
401
|
mode: options.mode,
|
|
360
402
|
model: options.model,
|
|
361
403
|
backend,
|
|
362
404
|
runtimes,
|
|
363
405
|
detected,
|
|
406
|
+
claudeCredentialsJson,
|
|
364
407
|
secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE,
|
|
365
408
|
keep: options.keepBox,
|
|
409
|
+
onData: (chunk) => router.push(chunk),
|
|
410
|
+
onPhase: (phase) => {
|
|
411
|
+
if (phase.kind === 'warmup') {
|
|
412
|
+
const label = `Leasing a ${phase.backend ?? 'hetzner'} box`;
|
|
413
|
+
spinner.start(`${label}…`);
|
|
414
|
+
const t0 = Date.now();
|
|
415
|
+
warmupTimer = setInterval(() => spinner.update(`${label}… (${Math.round((Date.now() - t0) / 1000)}s)`), 1000);
|
|
416
|
+
}
|
|
417
|
+
else if (phase.kind === 'ready') {
|
|
418
|
+
stopTimer();
|
|
419
|
+
spinner.stopAndPersist('✔', `Box ${phase.box.slug} ready${phase.box.ip ? ` (${phase.box.ip})` : ''} · ${Math.round(phase.elapsedMs / 1000)}s`);
|
|
420
|
+
spinner.start('Setting up box…');
|
|
421
|
+
}
|
|
422
|
+
else if (phase.kind === 'teardown') {
|
|
423
|
+
if (spinner.active)
|
|
424
|
+
spinner.stop();
|
|
425
|
+
}
|
|
426
|
+
},
|
|
366
427
|
});
|
|
428
|
+
router.end();
|
|
429
|
+
stopTimer();
|
|
430
|
+
if (spinner.active)
|
|
431
|
+
spinner.stop();
|
|
432
|
+
// Safety net: if setup failed before the agent ever ran, the setup log
|
|
433
|
+
// was only shown as transient spinner text — surface it so the error is
|
|
434
|
+
// diagnosable.
|
|
435
|
+
if (exitCode !== 0 && !router.sawAgent()) {
|
|
436
|
+
const log = router.setupLines();
|
|
437
|
+
if (log.length)
|
|
438
|
+
process.stderr.write(chalk.dim(log.join('\n')) + '\n');
|
|
439
|
+
}
|
|
367
440
|
console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${box.ip ? ` (${box.ip})` : ''}. Stop it: crabbox stop ${box.slug}`));
|
|
368
441
|
process.exit(exitCode === null ? 1 : exitCode);
|
|
369
442
|
}
|
|
370
443
|
catch (err) {
|
|
444
|
+
stopTimer();
|
|
445
|
+
if (spinner.active)
|
|
446
|
+
spinner.stopAndPersist('✖', chalk.red('Lease failed'));
|
|
447
|
+
const log = router.setupLines();
|
|
448
|
+
if (log.length && !router.sawAgent())
|
|
449
|
+
process.stderr.write(chalk.dim(log.join('\n')) + '\n');
|
|
371
450
|
console.error(chalk.red(err.message));
|
|
372
451
|
process.exit(1);
|
|
373
452
|
}
|
package/dist/commands/factory.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as path from 'path';
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
|
|
7
7
|
import { insertTask } from '../lib/cloud/store.js';
|
|
8
|
+
import { emit } from '../lib/events.js';
|
|
8
9
|
function requireFactoryUrl() {
|
|
9
10
|
const url = process.env.FACTORY_FLOOR_URL;
|
|
10
11
|
if (!url) {
|
|
@@ -79,6 +80,13 @@ Examples:
|
|
|
79
80
|
createdAt: now,
|
|
80
81
|
updatedAt: now,
|
|
81
82
|
});
|
|
83
|
+
emit('cloud.dispatch', {
|
|
84
|
+
module: 'factory',
|
|
85
|
+
taskId: result.cloud_execution_id,
|
|
86
|
+
ref,
|
|
87
|
+
linear_identifier: result.linear_identifier,
|
|
88
|
+
status: 'queued',
|
|
89
|
+
});
|
|
82
90
|
console.log(chalk.green(`Submitted ${result.linear_identifier} (${result.label})`));
|
|
83
91
|
console.log(` ticket ${result.ticket_id}`);
|
|
84
92
|
console.log(` execution ${result.cloud_execution_id}`);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents feed` -- list open blocks (decisions agents are waiting on).
|
|
3
|
+
*
|
|
4
|
+
* Aggregates block records from the local feed store and, with --host, from
|
|
5
|
+
* reachable remote hosts via SSH passthrough. Each block carries enough
|
|
6
|
+
* identity for `agents message` to route a reply back to the right agent.
|
|
7
|
+
*
|
|
8
|
+
* Default view groups by **outcome** (ticket / PR / worktree / Unassigned) so
|
|
9
|
+
* an operator sees dozens of deliverables, not ~1,100 agents. Pass `--flat`
|
|
10
|
+
* for the legacy per-agent list.
|
|
11
|
+
*/
|
|
12
|
+
import type { Command } from 'commander';
|
|
13
|
+
import { type OpenBlock } from '../lib/feed.js';
|
|
14
|
+
import { type OutcomeGroup, type SessionOutcomeHint } from '../lib/feed-outcome.js';
|
|
15
|
+
export declare const FEED_NO_FANOUT_ENV = "AGENTS_FEED_LOCAL";
|
|
16
|
+
export declare function parseRemoteFeed(stdout: string, machine: string): OpenBlock[];
|
|
17
|
+
/** Merge local and remote rows, keeping the first copy of a host/session block. */
|
|
18
|
+
export declare function mergeFeedBlocks(...groups: OpenBlock[][]): OpenBlock[];
|
|
19
|
+
export declare function shouldIncludeLocalFeed(hosts: string[] | undefined, self: string): boolean;
|
|
20
|
+
export declare function remoteFeedHostsToDial(hosts: string[] | undefined, self: string): string[] | undefined;
|
|
21
|
+
/** Human summary line for one outcome rollup. */
|
|
22
|
+
export declare function formatOutcomeHeader(group: OutcomeGroup): string;
|
|
23
|
+
/** Map active sessions into the lightweight hints outcome enrichment needs. */
|
|
24
|
+
export declare function sessionHintsFromActive(sessions: Array<{
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
agentId?: string;
|
|
27
|
+
ticket?: {
|
|
28
|
+
id?: string;
|
|
29
|
+
};
|
|
30
|
+
pr?: {
|
|
31
|
+
url?: string;
|
|
32
|
+
number?: number;
|
|
33
|
+
};
|
|
34
|
+
worktree?: {
|
|
35
|
+
slug?: string;
|
|
36
|
+
};
|
|
37
|
+
}>): SessionOutcomeHint[];
|
|
38
|
+
export declare function registerFeedCommand(program: Command): void;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { ensureFeedPublishHook, listBlocks, recordNotified } from '../lib/feed.js';
|
|
3
|
+
import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
|
|
4
|
+
import { classifyBlock, filterBlocksForFeed, suppressionDigest, } from '../lib/ask-classifier.js';
|
|
5
|
+
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
6
|
+
import { relTime } from '../lib/format.js';
|
|
7
|
+
import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
8
|
+
import { loadPolicy, applyPolicyToBlock, isPhoneUrgent } from '../lib/feed-policy.js';
|
|
9
|
+
import { notifyUrgentBlock } from '../lib/notify.js';
|
|
10
|
+
import { gcMailbox } from '../lib/mailbox-gc.js';
|
|
11
|
+
import { getActiveSessions } from '../lib/session/active.js';
|
|
12
|
+
import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
13
|
+
export const FEED_NO_FANOUT_ENV = 'AGENTS_FEED_LOCAL';
|
|
14
|
+
export function parseRemoteFeed(stdout, machine) {
|
|
15
|
+
let parsed;
|
|
16
|
+
try {
|
|
17
|
+
parsed = JSON.parse(stdout);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
if (!Array.isArray(parsed))
|
|
23
|
+
return [];
|
|
24
|
+
const blocks = [];
|
|
25
|
+
for (const item of parsed) {
|
|
26
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
27
|
+
continue;
|
|
28
|
+
const block = item;
|
|
29
|
+
if (!block.blockId || !block.sessionId || !block.mailboxId || !block.questions?.length)
|
|
30
|
+
continue;
|
|
31
|
+
blocks.push({ ...block, host: machine });
|
|
32
|
+
}
|
|
33
|
+
return blocks;
|
|
34
|
+
}
|
|
35
|
+
/** Merge local and remote rows, keeping the first copy of a host/session block. */
|
|
36
|
+
export function mergeFeedBlocks(...groups) {
|
|
37
|
+
const byIdentity = new Map();
|
|
38
|
+
for (const block of groups.flat()) {
|
|
39
|
+
const key = `${normalizeHost(block.host)}:${block.blockId}`;
|
|
40
|
+
if (!byIdentity.has(key))
|
|
41
|
+
byIdentity.set(key, block);
|
|
42
|
+
}
|
|
43
|
+
return [...byIdentity.values()].sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
|
|
44
|
+
}
|
|
45
|
+
function hostToken(host) {
|
|
46
|
+
return normalizeHost(host.split('@').pop() || host);
|
|
47
|
+
}
|
|
48
|
+
export function shouldIncludeLocalFeed(hosts, self) {
|
|
49
|
+
return !hosts?.length || hosts.some((host) => hostToken(host) === self);
|
|
50
|
+
}
|
|
51
|
+
export function remoteFeedHostsToDial(hosts, self) {
|
|
52
|
+
if (!hosts?.length)
|
|
53
|
+
return undefined;
|
|
54
|
+
return hosts.filter((host) => hostToken(host) !== self);
|
|
55
|
+
}
|
|
56
|
+
function renderBlock(b, localHost, indent = '') {
|
|
57
|
+
const host = b.host !== localHost ? chalk.yellow(` [${b.host}]`) : '';
|
|
58
|
+
const runtime = chalk.gray(b.runtime);
|
|
59
|
+
const age = chalk.gray(relTime(b.ts));
|
|
60
|
+
const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
|
|
61
|
+
const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
|
|
62
|
+
const cost = b.costOfDelay ? chalk.gray(`cost:${b.costOfDelay}`) : '';
|
|
63
|
+
console.log(`${indent}${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age} ${cls} ${consequence} ${cost}`.trimEnd());
|
|
64
|
+
for (const question of b.questions) {
|
|
65
|
+
const header = question.header ? chalk.gray(`[${question.header}] `) : '';
|
|
66
|
+
console.log(`${indent} ${header}${question.text}`);
|
|
67
|
+
if (question.options?.length) {
|
|
68
|
+
for (let i = 0; i < question.options.length; i++) {
|
|
69
|
+
const o = question.options[i];
|
|
70
|
+
const desc = o.description ? chalk.gray(` -- ${o.description}`) : '';
|
|
71
|
+
console.log(`${indent} ${chalk.dim(`${i + 1}.`)} ${o.label}${desc}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (b.ticket || b.pr || b.worktreeSlug) {
|
|
76
|
+
const meta = [b.ticket, b.pr, b.worktreeSlug].filter(Boolean).join(' ');
|
|
77
|
+
console.log(`${indent} ${chalk.gray(meta)}`);
|
|
78
|
+
}
|
|
79
|
+
if (b.answer) {
|
|
80
|
+
const verified = b.answer.verified ? chalk.green('✓') : chalk.yellow('?');
|
|
81
|
+
const who = b.answer.answeredFrom + (b.answer.answeredBy ? ` (${b.answer.answeredBy})` : '');
|
|
82
|
+
console.log(`${indent} ${chalk.green('answered')} by ${who} ${verified}`);
|
|
83
|
+
}
|
|
84
|
+
if (b.parkedAt) {
|
|
85
|
+
console.log(`${indent} ${chalk.red('hard-parked')} ${relTime(b.parkedAt)}`);
|
|
86
|
+
}
|
|
87
|
+
if (b.defaultedAt) {
|
|
88
|
+
console.log(`${indent} ${chalk.yellow('defaulted')} ${relTime(b.defaultedAt)}`);
|
|
89
|
+
}
|
|
90
|
+
if (b.receipts && b.receipts.length > 0) {
|
|
91
|
+
const latest = b.receipts[b.receipts.length - 1];
|
|
92
|
+
console.log(`${indent} ${chalk.dim('delivery:')} ${latest.status}`);
|
|
93
|
+
}
|
|
94
|
+
if (b.continuedAt) {
|
|
95
|
+
console.log(`${indent} ${chalk.green('continued')} ${relTime(b.continuedAt)}`);
|
|
96
|
+
}
|
|
97
|
+
if (b.notifiedAt) {
|
|
98
|
+
console.log(`${indent} ${chalk.dim('notified')} ${relTime(b.notifiedAt)}`);
|
|
99
|
+
}
|
|
100
|
+
if (!b.answer && !b.parkedAt) {
|
|
101
|
+
console.log(`${indent} ${chalk.dim('reply:')} agents message ${b.mailboxId} "<answer>"`);
|
|
102
|
+
}
|
|
103
|
+
console.log();
|
|
104
|
+
}
|
|
105
|
+
/** Human summary line for one outcome rollup. */
|
|
106
|
+
export function formatOutcomeHeader(group) {
|
|
107
|
+
const { agents, open, answered, parked } = group.counts;
|
|
108
|
+
const parts = [
|
|
109
|
+
`${agents} agent${agents === 1 ? '' : 's'}`,
|
|
110
|
+
open > 0 ? `${open} needs you` : null,
|
|
111
|
+
answered > 0 ? `${answered} answered` : null,
|
|
112
|
+
parked > 0 ? `${parked} parked` : null,
|
|
113
|
+
].filter(Boolean);
|
|
114
|
+
return `${group.outcome.label} · ${parts.join(' · ')}`;
|
|
115
|
+
}
|
|
116
|
+
function renderOutcomeGroup(group, localHost) {
|
|
117
|
+
console.log(chalk.bold(formatOutcomeHeader(group)));
|
|
118
|
+
if (isUnambiguousOutcomeAnswer(group) && openBlocksForOutcome(group).length > 1) {
|
|
119
|
+
const ids = openBlocksForOutcome(group).map((b) => b.mailboxId).join(', ');
|
|
120
|
+
console.log(chalk.dim(` same question on ${openBlocksForOutcome(group).length} agents — fan-out safe: ${ids}`));
|
|
121
|
+
}
|
|
122
|
+
for (const b of group.blocks) {
|
|
123
|
+
renderBlock(b, localHost, ' ');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Map active sessions into the lightweight hints outcome enrichment needs. */
|
|
127
|
+
export function sessionHintsFromActive(sessions) {
|
|
128
|
+
return sessions.map((s) => ({
|
|
129
|
+
sessionId: s.sessionId,
|
|
130
|
+
agentId: s.agentId,
|
|
131
|
+
// Same precedence as mailboxIdForActiveSession (agentId ?? sessionId).
|
|
132
|
+
mailboxId: s.agentId ?? s.sessionId,
|
|
133
|
+
ticketId: s.ticket?.id,
|
|
134
|
+
prNumber: s.pr?.number,
|
|
135
|
+
prUrl: s.pr?.url,
|
|
136
|
+
worktreeSlug: s.worktree?.slug,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
export function registerFeedCommand(program) {
|
|
140
|
+
program
|
|
141
|
+
.command('feed')
|
|
142
|
+
.description('List open blocks -- decisions agents are waiting on (grouped by outcome)')
|
|
143
|
+
.option('--json', 'Output as JSON (each block stamped with its outcome + ask class)')
|
|
144
|
+
.option('--flat', 'List one block per agent instead of grouping by outcome')
|
|
145
|
+
.option('--all', 'Include stalls/FYIs that policy would suppress (default: hide them)')
|
|
146
|
+
.option('--local', 'Only this machine -- skip the cross-machine SSH fan-out')
|
|
147
|
+
.option('-H, --host <target...>', 'Scope to remote machine(s) over SSH; repeatable')
|
|
148
|
+
.option('--device <target...>', 'Alias for --host; repeatable')
|
|
149
|
+
.option('--dispatch', 'Run stall suppression + default-on-no-answer policy and urgent notifications')
|
|
150
|
+
.action(async (opts) => {
|
|
151
|
+
if (opts.device?.length)
|
|
152
|
+
opts.host = [...(opts.host ?? []), ...opts.device];
|
|
153
|
+
const self = machineId();
|
|
154
|
+
const includeLocal = shouldIncludeLocalFeed(opts.host, self);
|
|
155
|
+
const setupWarnings = [];
|
|
156
|
+
if (includeLocal) {
|
|
157
|
+
const hookInstall = ensureFeedPublishHook();
|
|
158
|
+
if (hookInstall.error) {
|
|
159
|
+
setupWarnings.push(hookInstall.error);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const [{ iterHooksCapableVersions, parseHookManifest, registerHooksToSettings }, { getVersionHomePath }] = await Promise.all([
|
|
163
|
+
import('../lib/hooks.js'),
|
|
164
|
+
import('../lib/versions.js'),
|
|
165
|
+
]);
|
|
166
|
+
const manifest = parseHookManifest({ warn: false });
|
|
167
|
+
for (const { agent, version } of iterHooksCapableVersions({ agent: 'claude' })) {
|
|
168
|
+
const result = registerHooksToSettings(agent, getVersionHomePath(agent, version), manifest);
|
|
169
|
+
if (result.errors.length > 0) {
|
|
170
|
+
setupWarnings.push(`${agent}@${version}: ${result.errors.join('; ')}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Active sessions feed both the GC sweep and outcome enrichment (ticket/PR).
|
|
176
|
+
let sessions = [];
|
|
177
|
+
if (includeLocal) {
|
|
178
|
+
sessions = await getActiveSessions();
|
|
179
|
+
}
|
|
180
|
+
if (opts.dispatch && includeLocal) {
|
|
181
|
+
// Liveness sweep: drop messages to dead agents and retire stale blocks
|
|
182
|
+
// before we render the feed.
|
|
183
|
+
const activeBoxIds = new Set(sessions.map(mailboxIdForActiveSession).filter((id) => !!id));
|
|
184
|
+
const gcResult = gcMailbox(activeBoxIds);
|
|
185
|
+
if (gcResult.blocksRemoved > 0 || gcResult.messagesDroppedDead > 0) {
|
|
186
|
+
console.log(chalk.yellow(`gc: ${gcResult.messagesDroppedDead} dead messages, ${gcResult.blocksRemoved} stale blocks removed`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const localBlocks = includeLocal ? listBlocks() : [];
|
|
190
|
+
let blocks = localBlocks;
|
|
191
|
+
const forceLocal = opts.local === true || process.env[FEED_NO_FANOUT_ENV] === '1';
|
|
192
|
+
if (!forceLocal) {
|
|
193
|
+
const remoteHosts = remoteFeedHostsToDial(opts.host, self);
|
|
194
|
+
if (!opts.host?.length || (remoteHosts && remoteHosts.length > 0)) {
|
|
195
|
+
const remote = await gatherRemoteAgentsJson({
|
|
196
|
+
// Bare --json stays a block array so older peers and scripts keep working.
|
|
197
|
+
args: ['feed', '--json'],
|
|
198
|
+
noFanoutEnv: FEED_NO_FANOUT_ENV,
|
|
199
|
+
hosts: remoteHosts,
|
|
200
|
+
parse: parseRemoteFeed,
|
|
201
|
+
});
|
|
202
|
+
blocks = mergeFeedBlocks(localBlocks, remote.items);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Fill missing ticket/PR/worktree from live session meta so outcome keys
|
|
206
|
+
// land even when the publish hook had no deliverable stamp.
|
|
207
|
+
if (sessions.length > 0) {
|
|
208
|
+
blocks = enrichBlocksFromSessions(blocks, sessionHintsFromActive(sessions));
|
|
209
|
+
}
|
|
210
|
+
// Stall suppression (RUSH-1477): auto-answer "should I…?" / "what's next?"
|
|
211
|
+
// so they never render. Applied on every feed read unless --all; --dispatch
|
|
212
|
+
// also applies so unattended ticks drain the stall backlog.
|
|
213
|
+
const filter = filterBlocksForFeed(blocks, {
|
|
214
|
+
apply: !opts.all || opts.dispatch === true,
|
|
215
|
+
});
|
|
216
|
+
if (!opts.all) {
|
|
217
|
+
blocks = filter.surfaced;
|
|
218
|
+
}
|
|
219
|
+
const digest = suppressionDigest(filter);
|
|
220
|
+
if (digest && !opts.json) {
|
|
221
|
+
console.log(chalk.dim(digest));
|
|
222
|
+
}
|
|
223
|
+
if (opts.dispatch) {
|
|
224
|
+
const policy = loadPolicy();
|
|
225
|
+
const now = new Date();
|
|
226
|
+
for (const b of blocks) {
|
|
227
|
+
const result = applyPolicyToBlock(b, policy, now);
|
|
228
|
+
if (result.action !== 'none') {
|
|
229
|
+
console.log(`${chalk.yellow('policy')} ${b.blockId}: ${result.action}`);
|
|
230
|
+
}
|
|
231
|
+
if (isPhoneUrgent(b, policy)) {
|
|
232
|
+
const notifyResult = await notifyUrgentBlock(b, { dryRun: opts.json });
|
|
233
|
+
if (notifyResult.ok && !notifyResult.skipped) {
|
|
234
|
+
recordNotified(b.blockId);
|
|
235
|
+
console.log(`${chalk.green('notified')} ${b.blockId}`);
|
|
236
|
+
}
|
|
237
|
+
else if (notifyResult.error) {
|
|
238
|
+
console.error(chalk.yellow(`Notification failed for ${b.blockId}: ${notifyResult.error}`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const warning of setupWarnings) {
|
|
244
|
+
console.error(chalk.yellow(`Feed hook setup warning: ${warning}`));
|
|
245
|
+
}
|
|
246
|
+
if (opts.json) {
|
|
247
|
+
// Always a block array (stamped with outcome + ask class) so remote fan-out
|
|
248
|
+
// and scripts keep a stable contract. Human grouping is text-only.
|
|
249
|
+
const stamped = stampBlockOutcomes(blocks).map((b) => ({
|
|
250
|
+
...b,
|
|
251
|
+
ask: classifyBlock(b),
|
|
252
|
+
}));
|
|
253
|
+
console.log(JSON.stringify(stamped, null, 2));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (blocks.length === 0) {
|
|
257
|
+
console.log(chalk.gray(digest ? 'No open blocks after stall suppression.' : 'No open blocks.'));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (opts.flat) {
|
|
261
|
+
console.log(chalk.bold(`${blocks.length} open block${blocks.length === 1 ? '' : 's'}:\n`));
|
|
262
|
+
for (const b of blocks)
|
|
263
|
+
renderBlock(b, self);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const groups = groupBlocksByOutcome(blocks);
|
|
267
|
+
const openOutcomes = groups.filter((g) => g.counts.open > 0).length;
|
|
268
|
+
console.log(chalk.bold(`${groups.length} outcome${groups.length === 1 ? '' : 's'} · ${blocks.length} block${blocks.length === 1 ? '' : 's'}` +
|
|
269
|
+
(openOutcomes > 0 ? ` · ${openOutcomes} need you` : '') +
|
|
270
|
+
':\n'));
|
|
271
|
+
for (const g of groups)
|
|
272
|
+
renderOutcomeGroup(g, self);
|
|
273
|
+
});
|
|
274
|
+
}
|
package/dist/commands/logs.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agents logs` — unified, discoverable run-log viewer.
|
|
2
|
+
* `agents logs` — unified, discoverable run-log viewer + audit trail.
|
|
3
3
|
*
|
|
4
4
|
* Resolves a run across two substrates and shows (or `-f` follows) its log:
|
|
5
5
|
* - host-dispatch tasks (`agents run --host`) → combined-stdout log, offset-tailed
|
|
6
6
|
* - sessions (the local index) → transcript, tailed via the sessions tailer
|
|
7
7
|
*
|
|
8
|
+
* Subcommands:
|
|
9
|
+
* - `agents logs audit` — read the structured audit/event log
|
|
10
|
+
* - `agents logs stats` — show aggregate audit statistics
|
|
11
|
+
*
|
|
8
12
|
* Concise by default: a bare `agents logs <id>` prints the same summary digest as
|
|
9
13
|
* `agents sessions <id>` — cheap for an agent to glance at. The token-heavy full
|
|
10
14
|
* transcript / raw stdout is opt-in behind `--full` (alias `-m/--markdown`).
|