@phnx-labs/agents-cli 1.22.68 → 1.22.70
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 +46 -0
- package/README.md +17 -14
- package/dist/bootstrap.js +3 -0
- package/dist/commands/accounts.js +23 -7
- package/dist/commands/exec.js +33 -23
- package/dist/commands/feed.js +20 -7
- package/dist/commands/import.js +2 -2
- package/dist/commands/models.js +2 -2
- package/dist/commands/monitors.js +3 -0
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/rules.js +1 -1
- package/dist/commands/send.js +35 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/share.js +43 -14
- package/dist/commands/ssh.js +205 -2
- package/dist/commands/traces.js +1 -1
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-registry.d.ts +3 -3
- package/dist/lib/account-registry.js +25 -7
- package/dist/lib/accounting/usage.d.ts +7 -2
- package/dist/lib/accounting/usage.js +142 -10
- package/dist/lib/acp/client.d.ts +1 -1
- package/dist/lib/acp/client.js +12 -1
- package/dist/lib/acp/harnesses.js +1 -1
- package/dist/lib/add-dir.js +0 -2
- package/dist/lib/agent-cli-commands.js +0 -2
- package/dist/lib/agent-spec/agents.d.ts +1 -1
- package/dist/lib/agent-spec/agents.js +2 -83
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/ipc.js +9 -1
- package/dist/lib/browser/remote-control.d.ts +6 -3
- package/dist/lib/browser/remote-control.js +6 -3
- package/dist/lib/browser/service.d.ts +4 -1
- package/dist/lib/browser/service.js +7 -1
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/channels/registry.d.ts +6 -0
- package/dist/lib/channels/send.d.ts +11 -2
- package/dist/lib/channels/send.js +11 -2
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/cloud/rush.d.ts +10 -2
- package/dist/lib/cloud/rush.js +15 -9
- package/dist/lib/daemon/daemon.js +28 -8
- package/dist/lib/daemon/runner.js +6 -2
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.d.ts +2 -2
- package/dist/lib/exec.js +7 -34
- package/dist/lib/feed-broadcast.d.ts +47 -5
- package/dist/lib/feed-broadcast.js +127 -24
- package/dist/lib/hooks/install.js +0 -87
- package/dist/lib/installations/strategies.js +1 -1
- package/dist/lib/mcp-registry.js +0 -13
- package/dist/lib/mcp.js +2 -2
- package/dist/lib/model-tiers.js +1 -1
- package/dist/lib/models.js +0 -63
- package/dist/lib/monitors/engine.js +18 -0
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/notify.d.ts +11 -0
- package/dist/lib/notify.js +17 -4
- package/dist/lib/owner-message.d.ts +55 -0
- package/dist/lib/owner-message.js +64 -0
- package/dist/lib/permissions-registry.d.ts +0 -2
- package/dist/lib/permissions-registry.js +3 -50
- package/dist/lib/permissions.d.ts +3 -17
- package/dist/lib/permissions.js +4 -73
- package/dist/lib/run-trace-sync.d.ts +15 -0
- package/dist/lib/run-trace-sync.js +43 -21
- package/dist/lib/rush-session.d.ts +19 -0
- package/dist/lib/rush-session.js +24 -0
- package/dist/lib/secrets/drivers/rush.js +2 -1
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/session/active.d.ts +10 -0
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +12 -1
- package/dist/lib/session/db.js +20 -1
- package/dist/lib/session/discover.js +81 -1
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +7 -1
- package/dist/lib/session/prompt.js +12 -2
- package/dist/lib/session/recovery.d.ts +21 -12
- package/dist/lib/session/recovery.js +29 -11
- package/dist/lib/session/remote/watch.js +5 -2
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +13 -3
- package/dist/lib/share/publish.js +19 -15
- package/dist/lib/share/worker-template.js +5 -1
- package/dist/lib/sink-format.d.ts +34 -0
- package/dist/lib/sink-format.js +17 -0
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/staleness/detectors/permissions.js +0 -20
- package/dist/lib/staleness/writers/commands.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +2 -2
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/subagents-registry.js +2 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -37
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
|
@@ -2249,7 +2249,15 @@ function stopDaemonLocked() {
|
|
|
2249
2249
|
// any service-manager teardown or direct signal and repairs only under lock.
|
|
2250
2250
|
const pid = resolveLiveDaemonPid(true);
|
|
2251
2251
|
const browserSock = process.platform === 'win32' ? null : getBrowserIpcSocketPath();
|
|
2252
|
-
|
|
2252
|
+
// Capture the socket's inode INDEPENDENTLY of whether a live pid resolved
|
|
2253
|
+
// (PHNX-3618). A daemon that died between the CLI liveness precheck and this
|
|
2254
|
+
// locked read leaves resolveLiveDaemonPid() returning null while its ungraceful
|
|
2255
|
+
// exit left the binding on disk — capturing only when `pid !== null` meant that
|
|
2256
|
+
// genuinely stale socket could never be reclaimed and was reported "ownership
|
|
2257
|
+
// could not be verified". The inode is the successor guard: a fresh daemon that
|
|
2258
|
+
// rebinds during the stop gets a new inode, so a later identity match still
|
|
2259
|
+
// proves this exact binding is the one we captured, never a successor's.
|
|
2260
|
+
const browserSockOwner = browserSock ? readPathIdentity(browserSock) : null;
|
|
2253
2261
|
if (platform === 'darwin') {
|
|
2254
2262
|
const plistPath = getLaunchdPlistPath();
|
|
2255
2263
|
if (fs.existsSync(plistPath)) {
|
|
@@ -2380,8 +2388,19 @@ function stopDaemonLocked() {
|
|
|
2380
2388
|
// ungracefully (killTree) and left a stale binding — the owner is provably
|
|
2381
2389
|
// dead, so reclaim it and report.
|
|
2382
2390
|
if (process.platform !== 'win32') {
|
|
2391
|
+
// A binding is provably stale — and reclaimable — when the inode on disk is
|
|
2392
|
+
// still the exact one captured under this lock AND no daemon (the signalled
|
|
2393
|
+
// target OR any successor for this state dir) survives to own it. Keying the
|
|
2394
|
+
// "a daemon still owns it" test on the whole survivor set rather than only the
|
|
2395
|
+
// captured target is what lets a daemon that died BEFORE this stop (pid ===
|
|
2396
|
+
// null, PHNX-3618) still have its stale socket reclaimed, while a live
|
|
2397
|
+
// successor — which either rebound the inode or shows up as a survivor — is
|
|
2398
|
+
// never touched (PHNX-3607's never-delete-a-successor invariant).
|
|
2399
|
+
const aDaemonSurvives = survivors.length > 0 || unverifiedSurvivors.length > 0;
|
|
2383
2400
|
if (browserSock && fs.existsSync(browserSock)) {
|
|
2384
|
-
|
|
2401
|
+
const ownsCapturedBinding = browserSockOwner !== null
|
|
2402
|
+
&& pathIdentityMatches(browserSock, browserSockOwner);
|
|
2403
|
+
if (ownsCapturedBinding && !aDaemonSurvives) {
|
|
2385
2404
|
try {
|
|
2386
2405
|
fs.unlinkSync(browserSock);
|
|
2387
2406
|
}
|
|
@@ -2393,13 +2412,14 @@ function stopDaemonLocked() {
|
|
|
2393
2412
|
else
|
|
2394
2413
|
released.push('browser IPC socket (reclaimed)');
|
|
2395
2414
|
}
|
|
2415
|
+
else if (aDaemonSurvives) {
|
|
2416
|
+
surviving.push('browser IPC socket still owned by a surviving daemon');
|
|
2417
|
+
}
|
|
2396
2418
|
else {
|
|
2397
|
-
// The path was absent when
|
|
2398
|
-
//
|
|
2399
|
-
// invocation does not own the current binding and must leave it
|
|
2400
|
-
surviving.push(
|
|
2401
|
-
? 'browser IPC socket still owned by a surviving daemon'
|
|
2402
|
-
: 'browser IPC socket ownership could not be verified');
|
|
2419
|
+
// The path was absent when this transaction captured the target (a
|
|
2420
|
+
// successor bound it afterward) or a successor replaced the inode. Either
|
|
2421
|
+
// way this invocation does not own the current binding and must leave it.
|
|
2422
|
+
surviving.push('browser IPC socket ownership could not be verified');
|
|
2403
2423
|
}
|
|
2404
2424
|
}
|
|
2405
2425
|
else {
|
|
@@ -922,8 +922,12 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
|
|
|
922
922
|
if (config.account && !explicitCredential) {
|
|
923
923
|
// A native routine account is named by its durable name; the version matcher
|
|
924
924
|
// keys on the identity (email/accountKey), so translate before resolving,
|
|
925
|
-
// and refuse a login that belongs to a different harness.
|
|
926
|
-
|
|
925
|
+
// and refuse a login that belongs to a different harness. Scope the lookup to
|
|
926
|
+
// the routine's own harness: `identityLabel` defaults to the login's email, so
|
|
927
|
+
// `account: <email>` matches every harness that identity is signed into, and
|
|
928
|
+
// un-scoped this resolved whichever row the store ordered first — then rejected
|
|
929
|
+
// it on the very next line.
|
|
930
|
+
const unified = findUnifiedAccount(config.account, meta, undefined, agent);
|
|
927
931
|
if (unified?.kind === 'native' && unified.agent !== agent) {
|
|
928
932
|
throw new Error(`Routine '${config.name}' account '${config.account}' is a ${unified.agent} login and cannot authenticate ${agent}.`);
|
|
929
933
|
}
|
|
@@ -12,9 +12,37 @@
|
|
|
12
12
|
* separate copy on purpose: the CLI does not import across packages.
|
|
13
13
|
*/
|
|
14
14
|
import type { DeviceProfile } from './registry.js';
|
|
15
|
-
/** Default per-device probe budget
|
|
16
|
-
*
|
|
15
|
+
/** Default per-device probe budget, for a device reachable over a DIRECT
|
|
16
|
+
* Tailscale path. Short enough that the list never hangs on a wedged box. */
|
|
17
17
|
export declare const PROBE_TIMEOUT_MS = 2500;
|
|
18
|
+
/**
|
|
19
|
+
* Probe budget for a device whose last handshake was DERP-relayed
|
|
20
|
+
* ({@link DeviceTailscale.direct} === false).
|
|
21
|
+
*
|
|
22
|
+
* This constant used to not exist: 2.5s was applied to every device and its
|
|
23
|
+
* docstring claimed to be "long enough for a cold relayed SSH handshake". That
|
|
24
|
+
* was false, and on a fleet with no direct paths it broke `--device auto`
|
|
25
|
+
* outright (PHNX-3682). Measured on a 9-box relayed fleet, probed in parallel
|
|
26
|
+
* with cold paths: 1686/1805/1914/2688/2706/2749/3082/5586/6588 ms — six of nine
|
|
27
|
+
* over budget, every one of them healthy (rc=0 within 10s). Warm, the same
|
|
28
|
+
* probes take 512-872ms, which is what made the failure intermittent.
|
|
29
|
+
*
|
|
30
|
+
* A relayed hop pays DERP path setup on top of the TCP+SSH handshake, so it
|
|
31
|
+
* gets the same budget the readiness probe already allows
|
|
32
|
+
* (`READY_PROBE_TIMEOUT_MS`) rather than the direct-path one.
|
|
33
|
+
*/
|
|
34
|
+
export declare const RELAYED_PROBE_TIMEOUT_MS = 8000;
|
|
35
|
+
/**
|
|
36
|
+
* The probe budget for one device. A relayed peer gets
|
|
37
|
+
* {@link RELAYED_PROBE_TIMEOUT_MS}; a direct (or unknown-path) peer keeps the
|
|
38
|
+
* tight {@link PROBE_TIMEOUT_MS}. Windows keeps its own larger budget, which
|
|
39
|
+
* already exceeds both.
|
|
40
|
+
*
|
|
41
|
+
* `direct` is only meaningful when a tailscale snapshot exists — a
|
|
42
|
+
* `via:"manual"` device never gets a peer entry, so absence is "unknown path",
|
|
43
|
+
* not "relayed", and must not silently widen every manual device's budget.
|
|
44
|
+
*/
|
|
45
|
+
export declare function probeBudgetMs(device: DeviceProfile): number;
|
|
18
46
|
/** Windows probe budget. The first CIM query of a PowerShell session pays a
|
|
19
47
|
* "Preparing modules for first use" cost on top of PowerShell startup, which
|
|
20
48
|
* routinely blows the 2.5s POSIX budget on a relayed connection. */
|
|
@@ -35,6 +63,14 @@ export declare function localProbeInvocation(platform: NodeJS.Platform): {
|
|
|
35
63
|
export interface DeviceStats {
|
|
36
64
|
host: string;
|
|
37
65
|
reachable: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The probe exceeded its budget rather than being refused or unresolvable.
|
|
68
|
+
* Only meaningful when `reachable` is false — it separates "this box did not
|
|
69
|
+
* answer in time" from "this box actively could not be reached", so callers
|
|
70
|
+
* report a slow link honestly instead of calling a healthy device offline
|
|
71
|
+
* (PHNX-3682).
|
|
72
|
+
*/
|
|
73
|
+
timedOut?: boolean;
|
|
38
74
|
loadAvg1?: number;
|
|
39
75
|
ncpu?: number;
|
|
40
76
|
/** Load normalized to core count (the "has room" number): loadAvg1 / ncpu *
|
|
@@ -13,9 +13,43 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { execFile } from 'child_process';
|
|
15
15
|
import { buildSshInvocation, writeAskpassShim } from './connect.js';
|
|
16
|
-
/** Default per-device probe budget
|
|
17
|
-
*
|
|
16
|
+
/** Default per-device probe budget, for a device reachable over a DIRECT
|
|
17
|
+
* Tailscale path. Short enough that the list never hangs on a wedged box. */
|
|
18
18
|
export const PROBE_TIMEOUT_MS = 2_500;
|
|
19
|
+
/**
|
|
20
|
+
* Probe budget for a device whose last handshake was DERP-relayed
|
|
21
|
+
* ({@link DeviceTailscale.direct} === false).
|
|
22
|
+
*
|
|
23
|
+
* This constant used to not exist: 2.5s was applied to every device and its
|
|
24
|
+
* docstring claimed to be "long enough for a cold relayed SSH handshake". That
|
|
25
|
+
* was false, and on a fleet with no direct paths it broke `--device auto`
|
|
26
|
+
* outright (PHNX-3682). Measured on a 9-box relayed fleet, probed in parallel
|
|
27
|
+
* with cold paths: 1686/1805/1914/2688/2706/2749/3082/5586/6588 ms — six of nine
|
|
28
|
+
* over budget, every one of them healthy (rc=0 within 10s). Warm, the same
|
|
29
|
+
* probes take 512-872ms, which is what made the failure intermittent.
|
|
30
|
+
*
|
|
31
|
+
* A relayed hop pays DERP path setup on top of the TCP+SSH handshake, so it
|
|
32
|
+
* gets the same budget the readiness probe already allows
|
|
33
|
+
* (`READY_PROBE_TIMEOUT_MS`) rather than the direct-path one.
|
|
34
|
+
*/
|
|
35
|
+
export const RELAYED_PROBE_TIMEOUT_MS = 8_000;
|
|
36
|
+
/**
|
|
37
|
+
* The probe budget for one device. A relayed peer gets
|
|
38
|
+
* {@link RELAYED_PROBE_TIMEOUT_MS}; a direct (or unknown-path) peer keeps the
|
|
39
|
+
* tight {@link PROBE_TIMEOUT_MS}. Windows keeps its own larger budget, which
|
|
40
|
+
* already exceeds both.
|
|
41
|
+
*
|
|
42
|
+
* `direct` is only meaningful when a tailscale snapshot exists — a
|
|
43
|
+
* `via:"manual"` device never gets a peer entry, so absence is "unknown path",
|
|
44
|
+
* not "relayed", and must not silently widen every manual device's budget.
|
|
45
|
+
*/
|
|
46
|
+
export function probeBudgetMs(device) {
|
|
47
|
+
if (device.shell === 'powershell')
|
|
48
|
+
return WIN_PROBE_TIMEOUT_MS;
|
|
49
|
+
return device.tailscale && device.tailscale.direct === false
|
|
50
|
+
? RELAYED_PROBE_TIMEOUT_MS
|
|
51
|
+
: PROBE_TIMEOUT_MS;
|
|
52
|
+
}
|
|
19
53
|
/** Windows probe budget. The first CIM query of a PowerShell session pays a
|
|
20
54
|
* "Preparing modules for first use" cost on top of PowerShell startup, which
|
|
21
55
|
* routinely blows the 2.5s POSIX budget on a relayed connection. */
|
|
@@ -227,10 +261,14 @@ export function probeDeviceStats(device, opts = {}) {
|
|
|
227
261
|
execFile('ssh', args, {
|
|
228
262
|
encoding: 'utf-8',
|
|
229
263
|
env: { ...process.env, ...env },
|
|
230
|
-
timeout: opts.timeoutMs ?? (
|
|
264
|
+
timeout: opts.timeoutMs ?? probeBudgetMs(device),
|
|
231
265
|
}, (err, stdout) => {
|
|
232
|
-
|
|
233
|
-
|
|
266
|
+
// execFile kills an over-budget child with a signal; that is a slow link,
|
|
267
|
+
// not an unreachable box, and the two must not read the same downstream.
|
|
268
|
+
if (err || !stdout) {
|
|
269
|
+
const timedOut = Boolean(err && err.killed);
|
|
270
|
+
return resolve(timedOut ? { host, reachable: false, timedOut, fetchedAt } : { host, reachable: false, fetchedAt });
|
|
271
|
+
}
|
|
234
272
|
resolve(isWin ? parseWinProbeOutput(host, stdout, fetchedAt) : parseProbeOutput(host, stdout, fetchedAt));
|
|
235
273
|
});
|
|
236
274
|
});
|
|
@@ -2,7 +2,7 @@ import { type DevicePlacementSignal } from '../teams/scheduler.js';
|
|
|
2
2
|
/** Why a candidate was dropped, for the fail-loud error message. */
|
|
3
3
|
export interface WorkerExclusion {
|
|
4
4
|
device: string;
|
|
5
|
-
reason: 'unreachable' | 'overloaded' | 'wrong-platform' | 'interactive';
|
|
5
|
+
reason: 'unreachable' | 'probe timed out' | 'overloaded' | 'wrong-platform' | 'interactive';
|
|
6
6
|
}
|
|
7
7
|
export interface WorkerPickPlan {
|
|
8
8
|
/** The chosen device name. Never the local box unless it is an auto-pool member. */
|
|
@@ -79,7 +79,10 @@ export async function resolveWorkerDevice(opts = {}) {
|
|
|
79
79
|
const eligible = onPlatform.filter((device) => {
|
|
80
80
|
const signal = signals.get(device);
|
|
81
81
|
if (signal?.reachable !== true) {
|
|
82
|
-
|
|
82
|
+
// A probe killed for exceeding its budget says nothing about whether the
|
|
83
|
+
// box is up — on a relayed fleet it usually is. Report the two apart so
|
|
84
|
+
// the message points at the link, not at a fleet outage (PHNX-3682).
|
|
85
|
+
excluded.push({ device, reason: signal?.timedOut ? 'probe timed out' : 'unreachable' });
|
|
83
86
|
return false;
|
|
84
87
|
}
|
|
85
88
|
if (signal.headroom === 'loaded') {
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare function headlessPlanStallCommand(args: {
|
|
|
44
44
|
* (every agent supports edit-like behavior as its default).
|
|
45
45
|
* - `plan` on an agent without a read-only mode degrades to the agent's
|
|
46
46
|
* safest native mode (`capabilities.modes[0]`, typically `edit`). Agents
|
|
47
|
-
* like antigravity
|
|
47
|
+
* like antigravity have no plan flag; hard-failing made
|
|
48
48
|
* multi-agent scripts (`--mode plan` for everyone) unusable and diverged
|
|
49
49
|
* from `agents teams add`, which already defaults to `edit`. Callers that
|
|
50
50
|
* care (the `agents run` CLI) must surface a warning when requested ≠
|
|
@@ -64,7 +64,7 @@ export declare function resolveMode(agent: AgentId, requested: Mode): Mode;
|
|
|
64
64
|
* its ExitPlanMode gate. For those agents, a headless plan request degrades to
|
|
65
65
|
* `auto` (kimi -p auto-runs; grok maps auto→edit via resolveMode) with a visible
|
|
66
66
|
* one-line stderr warning, mirroring the graceful plan→edit degrade antigravity
|
|
67
|
-
*
|
|
67
|
+
* get for having no plan flag at all. Interactive runs are never
|
|
68
68
|
* downgraded. This is the single source of truth shared by buildExecCommand
|
|
69
69
|
* (agents run / teams) and the routine runner.
|
|
70
70
|
*/
|
package/dist/lib/exec.js
CHANGED
|
@@ -40,6 +40,7 @@ import { resolveHarnessAdapter, stripForeignConfigDir } from './harness/index.js
|
|
|
40
40
|
import { resolveConfigVersion } from './harness/exec-config-version.js';
|
|
41
41
|
import { getAccountInfo } from './agents.js';
|
|
42
42
|
import { getUsageLookupKey, noteClaudeSessionLimit, noteClaudeOutOfCredits, clearClaudeAccountRefusal, parseClaudeSessionLimitReset } from './accounting/usage.js';
|
|
43
|
+
import { bootMark, flushBootProfile } from './boot-profile.js';
|
|
43
44
|
/**
|
|
44
45
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
45
46
|
*
|
|
@@ -95,7 +96,7 @@ export function headlessPlanStallCommand(args) {
|
|
|
95
96
|
* (every agent supports edit-like behavior as its default).
|
|
96
97
|
* - `plan` on an agent without a read-only mode degrades to the agent's
|
|
97
98
|
* safest native mode (`capabilities.modes[0]`, typically `edit`). Agents
|
|
98
|
-
* like antigravity
|
|
99
|
+
* like antigravity have no plan flag; hard-failing made
|
|
99
100
|
* multi-agent scripts (`--mode plan` for everyone) unusable and diverged
|
|
100
101
|
* from `agents teams add`, which already defaults to `edit`. Callers that
|
|
101
102
|
* care (the `agents run` CLI) must surface a warning when requested ≠
|
|
@@ -115,7 +116,7 @@ export function resolveMode(agent, requested) {
|
|
|
115
116
|
}
|
|
116
117
|
if (requested === 'plan') {
|
|
117
118
|
// No read-only mode on this agent. modes[0] is the declared safest mode
|
|
118
|
-
// (edit for antigravity
|
|
119
|
+
// (edit for antigravity/…). Prefer that over hard-fail so
|
|
119
120
|
// uniform multi-agent `--mode plan` dispatches still run.
|
|
120
121
|
return supported[0];
|
|
121
122
|
}
|
|
@@ -131,7 +132,7 @@ export function resolveMode(agent, requested) {
|
|
|
131
132
|
* its ExitPlanMode gate. For those agents, a headless plan request degrades to
|
|
132
133
|
* `auto` (kimi -p auto-runs; grok maps auto→edit via resolveMode) with a visible
|
|
133
134
|
* one-line stderr warning, mirroring the graceful plan→edit degrade antigravity
|
|
134
|
-
*
|
|
135
|
+
* get for having no plan flag at all. Interactive runs are never
|
|
135
136
|
* downgraded. This is the single source of truth shared by buildExecCommand
|
|
136
137
|
* (agents run / teams) and the routine runner.
|
|
137
138
|
*/
|
|
@@ -521,25 +522,6 @@ export const AGENT_COMMANDS = {
|
|
|
521
522
|
jsonFlags: ['--format', 'json'],
|
|
522
523
|
modelFlag: '--model',
|
|
523
524
|
},
|
|
524
|
-
// Oh My Pi (`omp`). Headless is the positional MESSAGES arg + `-p/--print`.
|
|
525
|
-
// Approval modes map to omp's `--approval-mode`: always-ask (read-only tools
|
|
526
|
-
// auto-approved, writes gated -> our `plan`), write (read + workspace writes
|
|
527
|
-
// auto-approved -> `edit`), yolo (all tiers auto-approved -> `skip`). JSON is
|
|
528
|
-
// omp's `--mode json` event stream. `--model` fuzzy-matches a provider/model
|
|
529
|
-
// selector. Native resume is `-r/--resume <id-prefix>`.
|
|
530
|
-
pi: {
|
|
531
|
-
base: ['omp'],
|
|
532
|
-
promptFlag: 'positional',
|
|
533
|
-
modeFlags: {
|
|
534
|
-
plan: ['--approval-mode', 'always-ask'],
|
|
535
|
-
edit: ['--approval-mode', 'write'],
|
|
536
|
-
skip: ['--approval-mode', 'yolo'],
|
|
537
|
-
},
|
|
538
|
-
jsonFlags: ['--mode', 'json'],
|
|
539
|
-
modelFlag: '--model',
|
|
540
|
-
printFlags: ['-p'],
|
|
541
|
-
resume: { flag: '--resume' },
|
|
542
|
-
},
|
|
543
525
|
openclaw: {
|
|
544
526
|
base: ['openclaw'],
|
|
545
527
|
promptFlag: 'positional',
|
|
@@ -583,18 +565,6 @@ export const AGENT_COMMANDS = {
|
|
|
583
565
|
},
|
|
584
566
|
modelFlag: '--model',
|
|
585
567
|
},
|
|
586
|
-
kiro: {
|
|
587
|
-
// Standalone hooks live under ~/.kiro/hooks/*.json and only fire on the
|
|
588
|
-
// v3 engine (opt-in via --v3; see https://kiro.dev/docs/cli/v3/hooks/).
|
|
589
|
-
// Without this flag agents-cli would write v3 hook files that never run.
|
|
590
|
-
base: ['kiro-cli', '--v3'],
|
|
591
|
-
promptFlag: 'positional',
|
|
592
|
-
modeFlags: {
|
|
593
|
-
// kiro-cli has no permission flags — edit is the default behavior.
|
|
594
|
-
edit: [],
|
|
595
|
-
},
|
|
596
|
-
modelFlag: '--model',
|
|
597
|
-
},
|
|
598
568
|
goose: {
|
|
599
569
|
base: ['goose', 'run'],
|
|
600
570
|
promptFlag: 'positional',
|
|
@@ -1733,6 +1703,7 @@ async function emitRunLaunch(ctx) {
|
|
|
1733
1703
|
}
|
|
1734
1704
|
}
|
|
1735
1705
|
async function spawnAgent(options) {
|
|
1706
|
+
bootMark('spawn-agent:enter');
|
|
1736
1707
|
// Assign a known session id up front for agents that accept one, so the
|
|
1737
1708
|
// launcher can record an EXACT pid -> session mapping (see pid-registry) —
|
|
1738
1709
|
// otherwise the headless `ag sessions --active` path can only guess
|
|
@@ -1867,6 +1838,7 @@ async function spawnAgent(options) {
|
|
|
1867
1838
|
});
|
|
1868
1839
|
if (tmuxWrap.kind === 'wrap') {
|
|
1869
1840
|
timer.mark('startup');
|
|
1841
|
+
flushBootProfile('spawn');
|
|
1870
1842
|
try {
|
|
1871
1843
|
const result = await runInTmux(options, executable, args);
|
|
1872
1844
|
timer.end({ exitCode: result.exitCode, status: result.exitCode === 0 ? 'success' : 'failed' });
|
|
@@ -1898,6 +1870,7 @@ async function spawnAgent(options) {
|
|
|
1898
1870
|
const useShell = process.platform === 'win32' && (!path.isAbsolute(executable) || executable.endsWith('.cmd'));
|
|
1899
1871
|
const spawnCommand = useShell ? composeWin32CommandLine(executable, args) : executable;
|
|
1900
1872
|
const spawnArgs = useShell ? [] : args;
|
|
1873
|
+
flushBootProfile('spawn');
|
|
1901
1874
|
const child = spawn(spawnCommand, spawnArgs, {
|
|
1902
1875
|
cwd: options.cwd || process.cwd(),
|
|
1903
1876
|
stdio,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta } from './types.js';
|
|
2
|
+
import { type SinkMessageFormat } from './sink-format.js';
|
|
2
3
|
/** How loudly a post asks to be heard. Ordered — `important` implies milestone. */
|
|
3
4
|
export type FeedPostLevel = 'milestone' | 'important';
|
|
4
5
|
/** Parse a `--level` value; anything unrecognized is a usage error, not a default. */
|
|
@@ -122,6 +123,17 @@ export interface PlannedSink {
|
|
|
122
123
|
to?: string;
|
|
123
124
|
/** Channel sink body — the composed `{message}` for this post. */
|
|
124
125
|
text?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Owner-alias sinks only: the post context + its `message:` template, carried
|
|
128
|
+
* so the owner fan-out can re-render the body PER DESTINATION — Slack in the
|
|
129
|
+
* policy gets `mrkdwn` labeled links while iMessage stays plain (PHNX-3698).
|
|
130
|
+
* `text` above is the plain default (dry-run display + the fallback when a
|
|
131
|
+
* destination has no resolvable provider); this drives the real send. A
|
|
132
|
+
* direct `channel:` sink resolves its one provider's format at plan time and
|
|
133
|
+
* needs neither field.
|
|
134
|
+
*/
|
|
135
|
+
ctx?: FeedBroadcastContext;
|
|
136
|
+
messageTemplate?: string;
|
|
125
137
|
}
|
|
126
138
|
export interface SinkOutcome {
|
|
127
139
|
name: string;
|
|
@@ -136,6 +148,22 @@ export interface SinkOutcome {
|
|
|
136
148
|
export declare function shortHost(host: string | undefined): string | undefined;
|
|
137
149
|
/** First 8 hex chars of a session id for the footer (readable, not a full uuid). */
|
|
138
150
|
export declare function shortSessionChunk(session: string | undefined): string | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* Tap-to-view link for the session behind a post: the addressable console page
|
|
153
|
+
* ({@link https://prix.dev/console/sessions/<id>}, prix/web). The footer already
|
|
154
|
+
* carries a short session crumb for disambiguation; this rides the link trail so
|
|
155
|
+
* the owner can open the full transcript straight from an iMessage broadcast
|
|
156
|
+
* instead of hunting for it in the console.
|
|
157
|
+
*
|
|
158
|
+
* Accepts any real, path-safe session id — a Claude/Codex UUID *and* a native
|
|
159
|
+
* `ses_…` id from OpenCode or another harness. The console shard uploader
|
|
160
|
+
* (`traces/sync.ts`) syncs sessions with no harness filter, so all of them are
|
|
161
|
+
* addressable; a UUID-only gate would silently drop the link for every non-Claude
|
|
162
|
+
* harness (the whole point of the link). Reject only an id that could not resolve:
|
|
163
|
+
* one with a path separator (URL-unsafe, via {@link isValidMailboxId}) or the bare
|
|
164
|
+
* 8-char footer crumb (a truncated id that would 404).
|
|
165
|
+
*/
|
|
166
|
+
export declare function sessionConsoleUrl(session: string | undefined): string | undefined;
|
|
139
167
|
/**
|
|
140
168
|
* Scrub em/en dashes from outbound phone copy (house rule + iMessage readability).
|
|
141
169
|
* Collapses whitespace; does not invent meaning.
|
|
@@ -148,10 +176,15 @@ export declare function scrubOutboundDashes(text: string): string;
|
|
|
148
176
|
*
|
|
149
177
|
* Agent name first; session chunk for disambiguation when many groks run;
|
|
150
178
|
* host last. Skip the uninformative default label `agent`.
|
|
179
|
+
*
|
|
180
|
+
* In `mrkdwn` the crumb (`agent/short`) becomes a Slack labeled link to the
|
|
181
|
+
* session's console page, so the human sentence reads identically while the
|
|
182
|
+
* crumb turns blue and taps through (PHNX-3698). `plain` keeps the bare sentence
|
|
183
|
+
* — it can't render a labeled link and must not dump the URL.
|
|
151
184
|
*/
|
|
152
|
-
export declare function composeBroadcastFooter(ctx: FeedBroadcastContext): string | undefined;
|
|
185
|
+
export declare function composeBroadcastFooter(ctx: FeedBroadcastContext, format?: SinkMessageFormat): string | undefined;
|
|
153
186
|
export declare function truncateBroadcastBody(body: string): string;
|
|
154
|
-
export declare function composeBroadcastMessage(ctx: FeedBroadcastContext): string;
|
|
187
|
+
export declare function composeBroadcastMessage(ctx: FeedBroadcastContext, format?: SinkMessageFormat): string;
|
|
155
188
|
/**
|
|
156
189
|
* Substitute `{placeholder}` tokens in an argv template. Returns undefined when
|
|
157
190
|
* the template needs a value this post does not have — the sink is then skipped
|
|
@@ -159,8 +192,13 @@ export declare function composeBroadcastMessage(ctx: FeedBroadcastContext): stri
|
|
|
159
192
|
* would otherwise comment on nothing.
|
|
160
193
|
*/
|
|
161
194
|
export declare function renderSinkArgv(template: string[], ctx: FeedBroadcastContext): string[] | undefined;
|
|
162
|
-
/**
|
|
163
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Render one channel-message template with the same fail-closed placeholder
|
|
197
|
+
* contract as argv. `format` (Slack `mrkdwn` vs `plain`) flows into the shared
|
|
198
|
+
* `{message}` var so a Slack sink gets labeled links and an iMessage/owner sink
|
|
199
|
+
* gets the plain sentence.
|
|
200
|
+
*/
|
|
201
|
+
export declare function renderSinkMessage(template: string, ctx: FeedBroadcastContext, format?: SinkMessageFormat): string | undefined;
|
|
164
202
|
/**
|
|
165
203
|
* Which sinks this post reaches, in config order. Pure — the dry-run listing and
|
|
166
204
|
* the real fan-out plan through here, so what `--dry-run` shows is what runs.
|
|
@@ -168,8 +206,12 @@ export declare function renderSinkMessage(template: string, ctx: FeedBroadcastCo
|
|
|
168
206
|
* A `channel:` sink is gated by the same `minLevel` rule as a `command:` sink —
|
|
169
207
|
* one level check for both shapes, so a dry-run plan is truthful regardless of
|
|
170
208
|
* which shape an operator's sink uses.
|
|
209
|
+
*
|
|
210
|
+
* `meta` is used only to resolve a channel name to its real provider for the
|
|
211
|
+
* mrkdwn/plain format decision (`notify.transports`), the same map delivery uses;
|
|
212
|
+
* it is optional so a test can plan without a config snapshot (identity mapping).
|
|
171
213
|
*/
|
|
172
|
-
export declare function planFeedBroadcast(config: FeedBroadcastConfig | undefined, ctx: FeedBroadcastContext): PlannedSink[];
|
|
214
|
+
export declare function planFeedBroadcast(config: FeedBroadcastConfig | undefined, ctx: FeedBroadcastContext, meta?: Meta): PlannedSink[];
|
|
173
215
|
/**
|
|
174
216
|
* The effective sink config for a post: the operator's `feed.broadcast`, or —
|
|
175
217
|
* when that is unset or empty — an implicit fallback straight to
|