@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
package/dist/lib/events.d.ts
CHANGED
|
@@ -20,6 +20,14 @@ export interface EventMeta {
|
|
|
20
20
|
tz: string;
|
|
21
21
|
tzName: string;
|
|
22
22
|
hostname: string;
|
|
23
|
+
/**
|
|
24
|
+
* Normalized, joinable device id (`machine-id.ts::machineId()`) — the same key
|
|
25
|
+
* `agents devices`/session-sync use, so an event can be matched to a device.
|
|
26
|
+
* `hostname` is the raw `os.hostname()`; `machineId` is `zion` for `Zion.local`.
|
|
27
|
+
* Optional on the type so legacy records (pre-provenance-floor) and the activity
|
|
28
|
+
* stream still parse; `emit()` always stamps it on the operational log.
|
|
29
|
+
*/
|
|
30
|
+
machineId?: string;
|
|
23
31
|
platform: NodeJS.Platform;
|
|
24
32
|
arch: string;
|
|
25
33
|
pid: number;
|
|
@@ -40,6 +48,10 @@ export interface EventPayload {
|
|
|
40
48
|
agent?: string;
|
|
41
49
|
version?: string;
|
|
42
50
|
sessionId?: string;
|
|
51
|
+
/** Spawn-time join key (AGENT_LAUNCH_ID) mapping this action to its launch. */
|
|
52
|
+
launchId?: string;
|
|
53
|
+
/** The session that spawned this one (AGENTS_PARENT_SESSION_ID) — lineage edge. */
|
|
54
|
+
parentSessionId?: string;
|
|
43
55
|
cwd?: string;
|
|
44
56
|
/** Top-level command group, e.g. 'teams', 'secrets' — the audit filter key. */
|
|
45
57
|
module?: string;
|
package/dist/lib/events.js
CHANGED
|
@@ -20,6 +20,28 @@ import { parseSshConnection } from './session/provenance.js';
|
|
|
20
20
|
import { ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
21
21
|
import { getUserAgentsDir } from './state.js';
|
|
22
22
|
import { resolveActor } from './actor.js';
|
|
23
|
+
import { machineId } from './machine-id.js';
|
|
24
|
+
/** Lazy perf warehouse write — avoids a hard cycle at module load. */
|
|
25
|
+
function recordPerfTiming(payload) {
|
|
26
|
+
try {
|
|
27
|
+
// Dynamic import keeps events.ts free of a load-time dependency on perf/db.
|
|
28
|
+
void import('./perf/spool.js').then(({ recordSample }) => {
|
|
29
|
+
recordSample({
|
|
30
|
+
kind: 'perf.timing',
|
|
31
|
+
label: payload.label,
|
|
32
|
+
durationMs: payload.durationMs,
|
|
33
|
+
status: payload.status,
|
|
34
|
+
agent: payload.agent,
|
|
35
|
+
agentVersion: payload.version,
|
|
36
|
+
sessionId: payload.sessionId,
|
|
37
|
+
cwd: payload.cwd,
|
|
38
|
+
});
|
|
39
|
+
}).catch(() => { });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// fail soft
|
|
43
|
+
}
|
|
44
|
+
}
|
|
23
45
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
24
46
|
// Resolved lazily: events.ts is imported transitively by most CLI surfaces, and
|
|
25
47
|
// import itself must stay side-effect free. Tests may override the exact path.
|
|
@@ -118,7 +140,7 @@ const SECRET_PATH = /\/(secrets|credentials|\.env|user\.yaml)\b/i;
|
|
|
118
140
|
const SENSITIVE_ARG_NAME = /password|secret|token|key|api[-_]?key|auth/i;
|
|
119
141
|
const SENSITIVE_PAYLOAD_KEY = /password|secret|token|api[-_]?key|auth/i;
|
|
120
142
|
const RESERVED_META_KEYS = new Set([
|
|
121
|
-
'ts', 'tz', 'tzName', 'hostname', 'platform', 'arch', 'pid', 'ppid',
|
|
143
|
+
'ts', 'tz', 'tzName', 'hostname', 'machineId', 'platform', 'arch', 'pid', 'ppid',
|
|
122
144
|
'event', 'level', 'caller', 'session', 'osUser', 'transport', 'sshClientIp',
|
|
123
145
|
'actor', 'kind',
|
|
124
146
|
]);
|
|
@@ -288,6 +310,24 @@ function auditOrigin() {
|
|
|
288
310
|
};
|
|
289
311
|
return _origin;
|
|
290
312
|
}
|
|
313
|
+
/** This machine's normalized device id, resolved once — it can't change mid-process. */
|
|
314
|
+
let _machineId;
|
|
315
|
+
function cachedMachineId() {
|
|
316
|
+
return (_machineId ??= machineId());
|
|
317
|
+
}
|
|
318
|
+
function resolveProvenance(env = process.env) {
|
|
319
|
+
const p = {};
|
|
320
|
+
const sessionId = env.AGENT_SESSION_ID || env.AGENTS_SESSION_ID;
|
|
321
|
+
if (sessionId)
|
|
322
|
+
p.sessionId = sessionId;
|
|
323
|
+
if (env.AGENTS_AGENT_NAME)
|
|
324
|
+
p.agent = env.AGENTS_AGENT_NAME;
|
|
325
|
+
if (env.AGENT_LAUNCH_ID)
|
|
326
|
+
p.launchId = env.AGENT_LAUNCH_ID;
|
|
327
|
+
if (env.AGENTS_PARENT_SESSION_ID)
|
|
328
|
+
p.parentSessionId = env.AGENTS_PARENT_SESSION_ID;
|
|
329
|
+
return p;
|
|
330
|
+
}
|
|
291
331
|
// ─── Core API ─────────────────────────────────────────────────────────────────
|
|
292
332
|
/**
|
|
293
333
|
* Emit a structured event to the append-only audit log.
|
|
@@ -303,11 +343,14 @@ export function emit(event, payload = {}) {
|
|
|
303
343
|
const caller = detectCaller();
|
|
304
344
|
const safePayload = sanitizePayload(payload);
|
|
305
345
|
const record = {
|
|
346
|
+
// Provenance floor first: env-sourced defaults an explicit payload overrides.
|
|
347
|
+
...resolveProvenance(),
|
|
306
348
|
...safePayload,
|
|
307
349
|
ts: new Date().toISOString(),
|
|
308
350
|
tz: getTimezoneOffset(),
|
|
309
351
|
tzName: getTimezoneName(),
|
|
310
352
|
hostname: os.hostname(),
|
|
353
|
+
machineId: cachedMachineId(),
|
|
311
354
|
platform: os.platform(),
|
|
312
355
|
arch: os.arch(),
|
|
313
356
|
pid: process.pid,
|
|
@@ -375,22 +418,42 @@ export function time(label, fn, payload = {}) {
|
|
|
375
418
|
const start = Date.now();
|
|
376
419
|
try {
|
|
377
420
|
const result = fn();
|
|
421
|
+
const durationMs = Date.now() - start;
|
|
378
422
|
emit('perf.timing', {
|
|
379
423
|
...payload,
|
|
380
424
|
label,
|
|
381
|
-
durationMs
|
|
425
|
+
durationMs,
|
|
382
426
|
status: 'success',
|
|
383
427
|
});
|
|
428
|
+
recordPerfTiming({
|
|
429
|
+
label,
|
|
430
|
+
durationMs,
|
|
431
|
+
status: 'success',
|
|
432
|
+
agent: payload.agent,
|
|
433
|
+
version: payload.version,
|
|
434
|
+
sessionId: payload.sessionId,
|
|
435
|
+
cwd: payload.cwd,
|
|
436
|
+
});
|
|
384
437
|
return result;
|
|
385
438
|
}
|
|
386
439
|
catch (err) {
|
|
440
|
+
const durationMs = Date.now() - start;
|
|
387
441
|
emit('perf.timing', {
|
|
388
442
|
...payload,
|
|
389
443
|
label,
|
|
390
|
-
durationMs
|
|
444
|
+
durationMs,
|
|
391
445
|
status: 'error',
|
|
392
446
|
error: err instanceof Error ? err.message : String(err),
|
|
393
447
|
});
|
|
448
|
+
recordPerfTiming({
|
|
449
|
+
label,
|
|
450
|
+
durationMs,
|
|
451
|
+
status: 'error',
|
|
452
|
+
agent: payload.agent,
|
|
453
|
+
version: payload.version,
|
|
454
|
+
sessionId: payload.sessionId,
|
|
455
|
+
cwd: payload.cwd,
|
|
456
|
+
});
|
|
394
457
|
throw err;
|
|
395
458
|
}
|
|
396
459
|
}
|
|
@@ -405,22 +468,42 @@ export async function timeAsync(label, fn, payload = {}) {
|
|
|
405
468
|
const start = Date.now();
|
|
406
469
|
try {
|
|
407
470
|
const result = await fn();
|
|
471
|
+
const durationMs = Date.now() - start;
|
|
408
472
|
emit('perf.timing', {
|
|
409
473
|
...payload,
|
|
410
474
|
label,
|
|
411
|
-
durationMs
|
|
475
|
+
durationMs,
|
|
412
476
|
status: 'success',
|
|
413
477
|
});
|
|
478
|
+
recordPerfTiming({
|
|
479
|
+
label,
|
|
480
|
+
durationMs,
|
|
481
|
+
status: 'success',
|
|
482
|
+
agent: payload.agent,
|
|
483
|
+
version: payload.version,
|
|
484
|
+
sessionId: payload.sessionId,
|
|
485
|
+
cwd: payload.cwd,
|
|
486
|
+
});
|
|
414
487
|
return result;
|
|
415
488
|
}
|
|
416
489
|
catch (err) {
|
|
490
|
+
const durationMs = Date.now() - start;
|
|
417
491
|
emit('perf.timing', {
|
|
418
492
|
...payload,
|
|
419
493
|
label,
|
|
420
|
-
durationMs
|
|
494
|
+
durationMs,
|
|
421
495
|
status: 'error',
|
|
422
496
|
error: err instanceof Error ? err.message : String(err),
|
|
423
497
|
});
|
|
498
|
+
recordPerfTiming({
|
|
499
|
+
label,
|
|
500
|
+
durationMs,
|
|
501
|
+
status: 'error',
|
|
502
|
+
agent: payload.agent,
|
|
503
|
+
version: payload.version,
|
|
504
|
+
sessionId: payload.sessionId,
|
|
505
|
+
cwd: payload.cwd,
|
|
506
|
+
});
|
|
424
507
|
throw err;
|
|
425
508
|
}
|
|
426
509
|
}
|
|
@@ -449,13 +532,22 @@ export function createTimer(label, payload = {}) {
|
|
|
449
532
|
},
|
|
450
533
|
end(endPayload = {}) {
|
|
451
534
|
const durationMs = Date.now() - start;
|
|
535
|
+
const merged = { ...payload, ...endPayload };
|
|
452
536
|
emit('perf.timing', {
|
|
453
|
-
...
|
|
454
|
-
...endPayload,
|
|
537
|
+
...merged,
|
|
455
538
|
label,
|
|
456
539
|
durationMs,
|
|
457
540
|
phases: marks,
|
|
458
541
|
});
|
|
542
|
+
recordPerfTiming({
|
|
543
|
+
label,
|
|
544
|
+
durationMs,
|
|
545
|
+
status: typeof merged.status === 'string' ? merged.status : undefined,
|
|
546
|
+
agent: merged.agent,
|
|
547
|
+
version: merged.version,
|
|
548
|
+
sessionId: merged.sessionId,
|
|
549
|
+
cwd: merged.cwd,
|
|
550
|
+
});
|
|
459
551
|
},
|
|
460
552
|
};
|
|
461
553
|
}
|
|
@@ -472,22 +564,42 @@ export function withTiming(label, fn, basePayload = {}) {
|
|
|
472
564
|
const start = Date.now();
|
|
473
565
|
try {
|
|
474
566
|
const result = await fn(...args);
|
|
567
|
+
const durationMs = Date.now() - start;
|
|
475
568
|
emit('perf.timing', {
|
|
476
569
|
...basePayload,
|
|
477
570
|
label,
|
|
478
|
-
durationMs
|
|
571
|
+
durationMs,
|
|
479
572
|
status: 'success',
|
|
480
573
|
});
|
|
574
|
+
recordPerfTiming({
|
|
575
|
+
label,
|
|
576
|
+
durationMs,
|
|
577
|
+
status: 'success',
|
|
578
|
+
agent: basePayload.agent,
|
|
579
|
+
version: basePayload.version,
|
|
580
|
+
sessionId: basePayload.sessionId,
|
|
581
|
+
cwd: basePayload.cwd,
|
|
582
|
+
});
|
|
481
583
|
return result;
|
|
482
584
|
}
|
|
483
585
|
catch (err) {
|
|
586
|
+
const durationMs = Date.now() - start;
|
|
484
587
|
emit('perf.timing', {
|
|
485
588
|
...basePayload,
|
|
486
589
|
label,
|
|
487
|
-
durationMs
|
|
590
|
+
durationMs,
|
|
488
591
|
status: 'error',
|
|
489
592
|
error: err instanceof Error ? err.message : String(err),
|
|
490
593
|
});
|
|
594
|
+
recordPerfTiming({
|
|
595
|
+
label,
|
|
596
|
+
durationMs,
|
|
597
|
+
status: 'error',
|
|
598
|
+
agent: basePayload.agent,
|
|
599
|
+
version: basePayload.version,
|
|
600
|
+
sessionId: basePayload.sessionId,
|
|
601
|
+
cwd: basePayload.cwd,
|
|
602
|
+
});
|
|
491
603
|
throw err;
|
|
492
604
|
}
|
|
493
605
|
};
|
|
@@ -756,6 +868,7 @@ export function getLogsPath() {
|
|
|
756
868
|
export function _resetForTest(overrideEventsPath) {
|
|
757
869
|
_eventsPath = overrideEventsPath;
|
|
758
870
|
_origin = undefined;
|
|
871
|
+
_machineId = undefined;
|
|
759
872
|
_chmoddedPath = undefined;
|
|
760
873
|
lastRotationCheck = 0;
|
|
761
874
|
}
|
package/dist/lib/exec.js
CHANGED
|
@@ -373,6 +373,16 @@ export function buildExecEnv(options) {
|
|
|
373
373
|
result.AGENT_SESSION_ID = options.sessionId;
|
|
374
374
|
result.AGENTS_SESSION_ID = options.sessionId;
|
|
375
375
|
}
|
|
376
|
+
// Lineage edge: the child's parent is THIS process's session (the spawner), so a
|
|
377
|
+
// sub-agent's events carry a walkable edge back to who spawned it. The event floor
|
|
378
|
+
// (events.ts::resolveProvenance) reads AGENTS_PARENT_SESSION_ID and stamps it on
|
|
379
|
+
// every event the child emits. `options.sessionId` is the CHILD's id, so read the
|
|
380
|
+
// spawner from the live env; guard a same-session resume from naming itself parent.
|
|
381
|
+
// Local-spawn scope here; forwarding it across the `--host` SSH hop is Phase 4.
|
|
382
|
+
const spawnerSessionId = process.env.AGENTS_SESSION_ID || process.env.AGENT_SESSION_ID;
|
|
383
|
+
if (spawnerSessionId && spawnerSessionId !== options.sessionId) {
|
|
384
|
+
result.AGENTS_PARENT_SESSION_ID = spawnerSessionId;
|
|
385
|
+
}
|
|
376
386
|
result.AGENTS_RUNTIME = resolveInteractive(options) ? 'terminal' : 'headless';
|
|
377
387
|
// So activity / feed posts stamp the right harness without re-detecting.
|
|
378
388
|
if (options.agent) {
|
|
@@ -28,7 +28,54 @@ export interface FeedBroadcastContext {
|
|
|
28
28
|
session?: string;
|
|
29
29
|
/** URLs attached to the post — the PR, the ticket, a shared plan. */
|
|
30
30
|
links?: string[];
|
|
31
|
+
/** Block-only: the block's stable id. Absent on a status post. */
|
|
32
|
+
blockId?: string;
|
|
33
|
+
/** Block-only: `approval` (has a safe default) or `decision` (needs a human). */
|
|
34
|
+
class?: string;
|
|
35
|
+
/** Block-only: cost-of-delay tag used by the urgency filter. */
|
|
36
|
+
cost?: string;
|
|
37
|
+
/** Block-only: the literal `agents focus <id>` command that unblocks it. */
|
|
38
|
+
focus?: string;
|
|
31
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Map an open block onto the broadcast context, so a block reaches the same sinks
|
|
42
|
+
* a post does instead of dying in the ledger.
|
|
43
|
+
*
|
|
44
|
+
* The `text` is the ask itself, front-loaded — a notification banner shows roughly
|
|
45
|
+
* two lines, and a phone message is scanned, not read. `focus` carries the literal
|
|
46
|
+
* command that unblocks it, so the message the operator receives contains the one
|
|
47
|
+
* action they have to take rather than making them go find the session.
|
|
48
|
+
*
|
|
49
|
+
* Level is always `important`: a block is by definition an agent that has stopped
|
|
50
|
+
* making progress, so there is no per-block level flag to get wrong.
|
|
51
|
+
*/
|
|
52
|
+
export declare function blockBroadcastContext(block: {
|
|
53
|
+
blockId: string;
|
|
54
|
+
sessionId: string;
|
|
55
|
+
host?: string;
|
|
56
|
+
questions?: Array<{
|
|
57
|
+
text?: string;
|
|
58
|
+
}>;
|
|
59
|
+
blockClass?: string;
|
|
60
|
+
costOfDelay?: string;
|
|
61
|
+
ticket?: string;
|
|
62
|
+
pr?: string;
|
|
63
|
+
}, extras?: {
|
|
64
|
+
project?: string;
|
|
65
|
+
agent?: string;
|
|
66
|
+
}): FeedBroadcastContext;
|
|
67
|
+
/**
|
|
68
|
+
* Why a declared block reached nobody, or undefined when it got through.
|
|
69
|
+
*
|
|
70
|
+
* Pure so the fail-loud contract is testable without driving the CLI — the
|
|
71
|
+
* original version lived inline in the command action and was consequently
|
|
72
|
+
* never covered, which is how a `--json` early-return quietly bypassed it.
|
|
73
|
+
*
|
|
74
|
+
* Only a TOTAL failure counts. One sink failing among several is a warning, not
|
|
75
|
+
* an error: the channels are redundant by design, and a dead `rush` login must
|
|
76
|
+
* not mask a delivered desktop notification.
|
|
77
|
+
*/
|
|
78
|
+
export declare function blockDeliveryFailure(blocked: boolean, outcomes: SinkOutcome[]): string | undefined;
|
|
32
79
|
export interface PlannedSink {
|
|
33
80
|
name: string;
|
|
34
81
|
argv: string[];
|
|
@@ -35,6 +35,61 @@ export function parseFeedPostLevel(raw) {
|
|
|
35
35
|
return 'important';
|
|
36
36
|
throw new Error(`Unknown --level '${raw}'. Use milestone or important.`);
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Map an open block onto the broadcast context, so a block reaches the same sinks
|
|
40
|
+
* a post does instead of dying in the ledger.
|
|
41
|
+
*
|
|
42
|
+
* The `text` is the ask itself, front-loaded — a notification banner shows roughly
|
|
43
|
+
* two lines, and a phone message is scanned, not read. `focus` carries the literal
|
|
44
|
+
* command that unblocks it, so the message the operator receives contains the one
|
|
45
|
+
* action they have to take rather than making them go find the session.
|
|
46
|
+
*
|
|
47
|
+
* Level is always `important`: a block is by definition an agent that has stopped
|
|
48
|
+
* making progress, so there is no per-block level flag to get wrong.
|
|
49
|
+
*/
|
|
50
|
+
export function blockBroadcastContext(block, extras = {}) {
|
|
51
|
+
const ask = block.questions?.[0]?.text?.trim() || 'agent is blocked';
|
|
52
|
+
const links = [block.pr].filter((l) => !!l && /^https?:\/\//i.test(l));
|
|
53
|
+
return {
|
|
54
|
+
text: ask,
|
|
55
|
+
level: 'important',
|
|
56
|
+
ticket: block.ticket,
|
|
57
|
+
project: extras.project,
|
|
58
|
+
agent: extras.agent,
|
|
59
|
+
host: block.host,
|
|
60
|
+
session: block.sessionId,
|
|
61
|
+
blockId: block.blockId,
|
|
62
|
+
class: block.blockClass,
|
|
63
|
+
cost: block.costOfDelay,
|
|
64
|
+
// Short id: `agents focus` matches on a prefix, and a full uuid in a phone
|
|
65
|
+
// message is noise the operator has to skip past to reach the verb.
|
|
66
|
+
focus: `agents focus ${block.sessionId.slice(0, 8)}`,
|
|
67
|
+
...(links.length ? { links } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Why a declared block reached nobody, or undefined when it got through.
|
|
72
|
+
*
|
|
73
|
+
* Pure so the fail-loud contract is testable without driving the CLI — the
|
|
74
|
+
* original version lived inline in the command action and was consequently
|
|
75
|
+
* never covered, which is how a `--json` early-return quietly bypassed it.
|
|
76
|
+
*
|
|
77
|
+
* Only a TOTAL failure counts. One sink failing among several is a warning, not
|
|
78
|
+
* an error: the channels are redundant by design, and a dead `rush` login must
|
|
79
|
+
* not mask a delivered desktop notification.
|
|
80
|
+
*/
|
|
81
|
+
export function blockDeliveryFailure(blocked, outcomes) {
|
|
82
|
+
if (!blocked)
|
|
83
|
+
return undefined;
|
|
84
|
+
if (outcomes.length === 0) {
|
|
85
|
+
return 'Block recorded but NOT delivered — no feed.broadcast sink configured.';
|
|
86
|
+
}
|
|
87
|
+
if (outcomes.every((o) => !o.ok)) {
|
|
88
|
+
const why = outcomes.map((o) => `${o.name}: ${o.error ?? 'failed'}`).join('; ');
|
|
89
|
+
return `Block recorded but NOT delivered — every feed.broadcast sink failed (${why}).`;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
38
93
|
const PLACEHOLDER = /\{([a-z]+)\}/g;
|
|
39
94
|
/**
|
|
40
95
|
* A human-facing one-liner for a messaging sink: what project, what happened,
|
|
@@ -45,7 +100,12 @@ const PLACEHOLDER = /\{([a-z]+)\}/g;
|
|
|
45
100
|
export function composeBroadcastMessage(ctx) {
|
|
46
101
|
const head = ctx.project ? `${ctx.project} · ${ctx.text}` : ctx.text;
|
|
47
102
|
const link = ctx.links?.find((l) => /^https?:\/\//i.test(l));
|
|
48
|
-
|
|
103
|
+
// A block's second line is the command that unblocks it. The operator reading
|
|
104
|
+
// this on a phone should not have to go find the session — the one action they
|
|
105
|
+
// must take travels with the ask. A status post has no such action, so it keeps
|
|
106
|
+
// the link there instead.
|
|
107
|
+
const tail = [ctx.focus, link].filter(Boolean);
|
|
108
|
+
return tail.length ? `${head}\n${tail.join('\n')}` : head;
|
|
49
109
|
}
|
|
50
110
|
/** The values a template may reference, resolved once per post. */
|
|
51
111
|
function templateVars(ctx) {
|
|
@@ -59,6 +119,10 @@ function templateVars(ctx) {
|
|
|
59
119
|
level: ctx.level,
|
|
60
120
|
links: ctx.links?.length ? ctx.links.join(' ') : undefined,
|
|
61
121
|
message: composeBroadcastMessage(ctx),
|
|
122
|
+
block: ctx.blockId,
|
|
123
|
+
class: ctx.class,
|
|
124
|
+
cost: ctx.cost,
|
|
125
|
+
focus: ctx.focus,
|
|
62
126
|
};
|
|
63
127
|
}
|
|
64
128
|
/**
|
package/dist/lib/feed-post.d.ts
CHANGED
|
@@ -9,6 +9,16 @@ export interface FeedPostInput {
|
|
|
9
9
|
sessionId?: string;
|
|
10
10
|
/** Generic artifacts to attach: local paths (copied for durability) or URLs. */
|
|
11
11
|
attach?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* The agent is STUCK, not merely reporting. Writes `status.blocked` instead of
|
|
14
|
+
* `status.posted`, and the caller pairs it with an OpenBlock so the ask stays
|
|
15
|
+
* answerable until someone resolves it.
|
|
16
|
+
*
|
|
17
|
+
* This is a state, not a volume: a blocked post is always broadcast at
|
|
18
|
+
* `important`, so an agent never has to decide the level as well. One thing to
|
|
19
|
+
* say is what makes the habit stick.
|
|
20
|
+
*/
|
|
21
|
+
blocked?: boolean;
|
|
12
22
|
/** Override activity root (tests). */
|
|
13
23
|
activityRoot?: string;
|
|
14
24
|
/**
|
package/dist/lib/feed-post.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import { spawnSync } from 'child_process';
|
|
18
|
-
import { appendActivityEvent,
|
|
18
|
+
import { appendActivityEvent, } from './activity.js';
|
|
19
|
+
import { resolveProjectNameForCwd, listProjectDefs } from './projects.js';
|
|
19
20
|
import { getHistoryDir } from './state.js';
|
|
20
21
|
import { machineId } from './machine-id.js';
|
|
21
22
|
import { isValidMailboxId } from './mailbox.js';
|
|
@@ -263,7 +264,12 @@ export function postFeedStatus(input) {
|
|
|
263
264
|
+ '(AGENT_SESSION_ID / AGENTS_MAILBOX_DIR / pid registry), or pass --session <id>.');
|
|
264
265
|
}
|
|
265
266
|
const ts = input.ts ?? new Date().toISOString();
|
|
266
|
-
|
|
267
|
+
// The post is written where the agent runs, so the cwd is a local path and
|
|
268
|
+
// gets full canonical resolution — a defined project's name wins (a post from
|
|
269
|
+
// any repo of a multi-repo project files under that project), else the repo
|
|
270
|
+
// key, matching how the timeline groups everything else. listProjectDefs is
|
|
271
|
+
// fail-open, so this costs one small readdir + YAML parse per post.
|
|
272
|
+
const project = resolveProjectNameForCwd(identity.cwd, listProjectDefs());
|
|
267
273
|
const attachments = buildAttachments(input.attach, {
|
|
268
274
|
copyRoot: input.attachmentsRoot ?? path.join(getHistoryDir(), 'attachments'),
|
|
269
275
|
sessionId: identity.sessionId,
|
|
@@ -271,7 +277,7 @@ export function postFeedStatus(input) {
|
|
|
271
277
|
});
|
|
272
278
|
const event = {
|
|
273
279
|
ts,
|
|
274
|
-
event: 'status.posted',
|
|
280
|
+
event: input.blocked ? 'status.blocked' : 'status.posted',
|
|
275
281
|
sessionId: identity.sessionId,
|
|
276
282
|
mailboxId: identity.mailboxId,
|
|
277
283
|
host: identity.host,
|
package/dist/lib/feed.d.ts
CHANGED
|
@@ -38,7 +38,20 @@ export interface OpenBlock {
|
|
|
38
38
|
runtime: string;
|
|
39
39
|
ts: string;
|
|
40
40
|
questions: BlockQuestion[];
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* How this block came to exist.
|
|
43
|
+
* question — an AskUserQuestion the harness surfaced
|
|
44
|
+
* notification — a permission/idle prompt the harness raised
|
|
45
|
+
* control — a synthetic card the feed itself computed (runaway, needy)
|
|
46
|
+
* declared — the AGENT decided it is stuck and said so (`feed post --blocked`)
|
|
47
|
+
*
|
|
48
|
+
* `declared` is the only kind that does not depend on the harness noticing
|
|
49
|
+
* anything. Every other kind is inferred from a harness event, and hook events
|
|
50
|
+
* are not portable across harnesses (only Claude fires Notification, only Codex
|
|
51
|
+
* fires PermissionRequest), so a declared block is the one signal every agent
|
|
52
|
+
* can raise — it is just a shell command.
|
|
53
|
+
*/
|
|
54
|
+
kind?: 'question' | 'notification' | 'control' | 'declared';
|
|
42
55
|
notificationType?: string;
|
|
43
56
|
ticket?: string;
|
|
44
57
|
pr?: string;
|
|
@@ -159,6 +172,39 @@ export declare function recordNotified(blockId: string, root?: string): void;
|
|
|
159
172
|
export declare function recordTerminalAnswer(blockId: string, root?: string): void;
|
|
160
173
|
/** Remove answered marker and receipts for a block (used by block removal/GC). */
|
|
161
174
|
export declare function clearBlockLifecycle(blockId: string, root?: string): void;
|
|
175
|
+
/** Identity of the agent declaring a block — the subset of `PostIdentity` it needs. */
|
|
176
|
+
export interface DeclaringAgent {
|
|
177
|
+
sessionId: string;
|
|
178
|
+
mailboxId: string;
|
|
179
|
+
host: string;
|
|
180
|
+
runtime: string;
|
|
181
|
+
}
|
|
182
|
+
export interface DeclareBlockInput {
|
|
183
|
+
/** What the agent needs from the user, front-loaded. */
|
|
184
|
+
text: string;
|
|
185
|
+
/** Answerable choices, if the ask is a pick-one. */
|
|
186
|
+
options?: string[];
|
|
187
|
+
/** A safe default makes this an approval; without one it is a decision. */
|
|
188
|
+
safeDefault?: string;
|
|
189
|
+
/** Minutes before the default-on-no-answer policy may fire. */
|
|
190
|
+
timeoutMinutes?: number;
|
|
191
|
+
ts?: string;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Build the block record for `agents feed post --blocked` — pure, so the shape is testable
|
|
195
|
+
* without touching the store or the broadcast layer.
|
|
196
|
+
*
|
|
197
|
+
* Class is derived, not asked for: a `--default` means the user could be absent
|
|
198
|
+
* and policy could still resolve it (approval); no default means only a human can
|
|
199
|
+
* choose (decision). `feed-policy.ts` reads exactly that distinction, so deriving
|
|
200
|
+
* it here keeps one rule in one place instead of letting a caller set a class that
|
|
201
|
+
* contradicts its own safeDefault.
|
|
202
|
+
*
|
|
203
|
+
* `costOfDelay: high` because a declared block is, by definition, an agent that
|
|
204
|
+
* has already stopped making progress — that is what makes it worth interrupting
|
|
205
|
+
* someone over, and what `feed --dispatch`'s urgency filter keys off.
|
|
206
|
+
*/
|
|
207
|
+
export declare function buildDeclaredBlock(agent: DeclaringAgent, input: DeclareBlockInput): OpenBlock;
|
|
162
208
|
/** Atomic write a block record to the feed store. Clears stale lifecycle state. */
|
|
163
209
|
export declare function publishBlock(block: OpenBlock, root?: string): void;
|
|
164
210
|
/** Read all block records. Returns them sorted by stable block filename. */
|
package/dist/lib/feed.js
CHANGED
|
@@ -236,6 +236,44 @@ export function clearBlockLifecycle(blockId, root) {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Build the block record for `agents feed post --blocked` — pure, so the shape is testable
|
|
241
|
+
* without touching the store or the broadcast layer.
|
|
242
|
+
*
|
|
243
|
+
* Class is derived, not asked for: a `--default` means the user could be absent
|
|
244
|
+
* and policy could still resolve it (approval); no default means only a human can
|
|
245
|
+
* choose (decision). `feed-policy.ts` reads exactly that distinction, so deriving
|
|
246
|
+
* it here keeps one rule in one place instead of letting a caller set a class that
|
|
247
|
+
* contradicts its own safeDefault.
|
|
248
|
+
*
|
|
249
|
+
* `costOfDelay: high` because a declared block is, by definition, an agent that
|
|
250
|
+
* has already stopped making progress — that is what makes it worth interrupting
|
|
251
|
+
* someone over, and what `feed --dispatch`'s urgency filter keys off.
|
|
252
|
+
*/
|
|
253
|
+
export function buildDeclaredBlock(agent, input) {
|
|
254
|
+
const text = input.text.trim().replace(/\s+/g, ' ');
|
|
255
|
+
if (!text) {
|
|
256
|
+
throw new Error('Block text is empty. Usage: agents feed post "what you need from the user" --blocked');
|
|
257
|
+
}
|
|
258
|
+
const options = (input.options ?? [])
|
|
259
|
+
.map((label) => label.trim())
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.map((label) => ({ label }));
|
|
262
|
+
return {
|
|
263
|
+
blockId: blockIdForSession(agent.sessionId),
|
|
264
|
+
sessionId: agent.sessionId,
|
|
265
|
+
mailboxId: agent.mailboxId,
|
|
266
|
+
host: agent.host,
|
|
267
|
+
runtime: agent.runtime,
|
|
268
|
+
ts: input.ts ?? new Date().toISOString(),
|
|
269
|
+
kind: 'declared',
|
|
270
|
+
questions: [{ text, header: 'Needs you', ...(options.length ? { options } : {}) }],
|
|
271
|
+
blockClass: input.safeDefault ? 'approval' : 'decision',
|
|
272
|
+
costOfDelay: 'high',
|
|
273
|
+
...(input.safeDefault ? { safeDefault: input.safeDefault } : {}),
|
|
274
|
+
...(input.timeoutMinutes !== undefined ? { timeoutMinutes: input.timeoutMinutes } : {}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
239
277
|
/** Atomic write a block record to the feed store. Clears stale lifecycle state. */
|
|
240
278
|
export function publishBlock(block, root) {
|
|
241
279
|
const dir = root ?? getFeedDir();
|
|
@@ -26,6 +26,8 @@ export interface HookShimPaths {
|
|
|
26
26
|
shimsDir?: string;
|
|
27
27
|
cacheDir?: string;
|
|
28
28
|
logsDir?: string;
|
|
29
|
+
/** Directory for the disposable perf warehouse + spool (default ~/.agents/.cache/perf). */
|
|
30
|
+
perfDir?: string;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* Generate (or refresh) the shim script for a hook. Idempotent — only writes
|