@phnx-labs/agents-cli 1.22.75 → 1.22.76
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 +117 -0
- package/README.md +20 -8
- package/dist/bootstrap.js +7 -7
- package/dist/cli/command-registry.js +5 -0
- package/dist/commands/artifacts-setup.js +1 -1
- package/dist/commands/artifacts.js +1 -1
- package/dist/commands/auth.js +7 -1
- package/dist/commands/browser.js +104 -10
- package/dist/commands/commands.js +7 -6
- package/dist/commands/config.js +27 -4
- package/dist/commands/cost.js +6 -4
- package/dist/commands/doctor.d.ts +6 -5
- package/dist/commands/doctor.js +32 -274
- package/dist/commands/exec.d.ts +2 -0
- package/dist/commands/exec.js +9 -2
- package/dist/commands/harness.d.ts +1 -0
- package/dist/commands/harness.js +11 -3
- package/dist/commands/hooks.js +7 -6
- package/dist/commands/mcp.js +7 -6
- package/dist/commands/memory.js +7 -7
- package/dist/commands/monitors.js +3 -2
- package/dist/commands/open.d.ts +25 -12
- package/dist/commands/open.js +24 -10
- package/dist/commands/permissions.js +7 -6
- package/dist/commands/plugins.js +21 -17
- package/dist/commands/route.js +33 -16
- package/dist/commands/rules.js +7 -12
- package/dist/commands/sessions-share.js +1 -1
- package/dist/commands/setup-watchdog.js +2 -2
- package/dist/commands/setup.js +22 -1
- package/dist/commands/share.js +26 -10
- package/dist/commands/skills.js +7 -6
- package/dist/commands/subagents.js +7 -6
- package/dist/commands/sync.js +81 -10
- package/dist/commands/view.js +4 -1
- package/dist/commands/watchdog.d.ts +1 -1
- package/dist/commands/watchdog.js +10 -10
- package/dist/commands/webhook.d.ts +4 -0
- package/dist/commands/webhook.js +22 -4
- package/dist/commands/workflows.js +7 -6
- package/dist/lib/accounting/rotate.d.ts +3 -1
- package/dist/lib/accounting/rotate.js +8 -4
- package/dist/lib/auth-health.d.ts +2 -0
- package/dist/lib/auth-health.js +2 -0
- package/dist/lib/browser/chrome.d.ts +21 -0
- package/dist/lib/browser/chrome.js +60 -3
- package/dist/lib/browser/drivers/local.d.ts +21 -0
- package/dist/lib/browser/drivers/local.js +102 -9
- package/dist/lib/browser/profiles.d.ts +29 -1
- package/dist/lib/browser/profiles.js +50 -1
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/config-keys.d.ts +7 -2
- package/dist/lib/config-keys.js +17 -2
- package/dist/lib/daemon/daemon.js +8 -0
- package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
- package/dist/lib/daemon/session-summarizer-service.js +39 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/daemon-ticks.d.ts +2 -2
- package/dist/lib/daemon-ticks.js +2 -1
- package/dist/lib/deeplink/register.js +10 -9
- package/dist/lib/deeplink/url.d.ts +4 -4
- package/dist/lib/deeplink/url.js +4 -4
- package/dist/lib/device-config.js +25 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -4
- package/dist/lib/devices/doctor-findings.js +14 -8
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/stats-cache.d.ts +4 -0
- package/dist/lib/devices/stats-cache.js +19 -0
- package/dist/lib/drift-sync.d.ts +3 -1
- package/dist/lib/drift-sync.js +16 -5
- package/dist/lib/exec.d.ts +2 -0
- package/dist/lib/exec.js +16 -1
- package/dist/lib/fleet-shared-state.d.ts +8 -0
- package/dist/lib/heal.d.ts +4 -3
- package/dist/lib/heal.js +5 -4
- package/dist/lib/hosts/ready.d.ts +1 -1
- package/dist/lib/hosts/ready.js +16 -4
- package/dist/lib/hosts/reconnect.js +4 -2
- package/dist/lib/identity/client.d.ts +6 -0
- package/dist/lib/identity/index.d.ts +16 -0
- package/dist/lib/identity/index.js +25 -1
- package/dist/lib/profiles.d.ts +2 -0
- package/dist/lib/profiles.js +28 -9
- package/dist/lib/reconcile-and-repair.d.ts +109 -0
- package/dist/lib/reconcile-and-repair.js +267 -0
- package/dist/lib/routers.d.ts +12 -1
- package/dist/lib/routers.js +30 -1
- package/dist/lib/scheduling/routines.js +8 -2
- package/dist/lib/session/active.d.ts +13 -0
- package/dist/lib/session/db.d.ts +47 -7
- package/dist/lib/session/db.js +114 -12
- package/dist/lib/session/mirror.js +58 -0
- package/dist/lib/session/remote/watch.js +22 -2
- package/dist/lib/session/session-cache.d.ts +19 -0
- package/dist/lib/session/session-cache.js +46 -0
- package/dist/lib/session/types.d.ts +34 -0
- package/dist/lib/share/backend.d.ts +6 -4
- package/dist/lib/share/backend.js +10 -8
- package/dist/lib/share/config.d.ts +4 -3
- package/dist/lib/share/config.js +10 -1
- package/dist/lib/share/delete.d.ts +1 -1
- package/dist/lib/share/delete.js +1 -1
- package/dist/lib/share/html.d.ts +1 -1
- package/dist/lib/share/html.js +1 -1
- package/dist/lib/share/provision.d.ts +1 -1
- package/dist/lib/share/provision.js +2 -2
- package/dist/lib/share/publish.d.ts +23 -7
- package/dist/lib/share/publish.js +58 -12
- package/dist/lib/share/worker-template.js +221 -60
- package/dist/lib/startup/command-registry.js +2 -2
- package/dist/lib/state.d.ts +15 -0
- package/dist/lib/state.js +29 -7
- package/dist/lib/summarizer/config.d.ts +46 -0
- package/dist/lib/summarizer/config.js +83 -0
- package/dist/lib/summarizer/pass.d.ts +45 -0
- package/dist/lib/summarizer/pass.js +112 -0
- package/dist/lib/summarizer/summarize.d.ts +68 -0
- package/dist/lib/summarizer/summarize.js +120 -0
- package/dist/lib/teams/agents.d.ts +4 -3
- package/dist/lib/teams/agents.js +12 -4
- package/dist/lib/teams/scheduler.d.ts +4 -2
- package/dist/lib/teams/scheduler.js +6 -6
- package/dist/lib/tmux/session.d.ts +2 -0
- package/dist/lib/tmux/session.js +7 -1
- package/dist/lib/types.d.ts +20 -0
- package/dist/lib/verbs.d.ts +23 -0
- package/dist/lib/verbs.js +24 -0
- package/dist/lib/view-types.d.ts +4 -0
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
|
@@ -30,6 +30,12 @@ export interface PhoenixSession {
|
|
|
30
30
|
access_token: string;
|
|
31
31
|
email?: string;
|
|
32
32
|
userId?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Hosted OAuth profile image (https URL), when Phoenix ID exposes one. Wins
|
|
35
|
+
* over the email-Gravatar fallback in share attribution. Optional forever —
|
|
36
|
+
* a missing value just means the Gravatar/initials fallback.
|
|
37
|
+
*/
|
|
38
|
+
avatarUrl?: string;
|
|
33
39
|
/** Unix ms; absent means the server did not scope the token's lifetime. */
|
|
34
40
|
expires_at?: number;
|
|
35
41
|
}
|
|
@@ -16,6 +16,8 @@ export interface WhoAmI {
|
|
|
16
16
|
userId: string;
|
|
17
17
|
email: string;
|
|
18
18
|
valid: true;
|
|
19
|
+
/** Hosted OAuth profile image, when Phoenix ID stores one for this user. */
|
|
20
|
+
avatar_url?: string;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* RFC 8628 poll outcomes. `pending` and `slow_down` are normal states of a
|
|
@@ -28,6 +30,10 @@ export type DevicePoll = {
|
|
|
28
30
|
user: {
|
|
29
31
|
email: string;
|
|
30
32
|
id: string;
|
|
33
|
+
/** Hosted OAuth profile image, when the provider exposed one to Phoenix ID. */
|
|
34
|
+
avatar_url?: string;
|
|
35
|
+
/** Google-style alias some providers use for the same field. */
|
|
36
|
+
picture?: string;
|
|
31
37
|
};
|
|
32
38
|
} | {
|
|
33
39
|
status: 'pending';
|
|
@@ -41,6 +47,16 @@ export type DevicePoll = {
|
|
|
41
47
|
export declare function startDeviceAuthorization(): Promise<DeviceAuthorization>;
|
|
42
48
|
export declare function pollDeviceToken(deviceCode: string): Promise<DevicePoll>;
|
|
43
49
|
export declare function fetchWhoAmI(token?: string): Promise<WhoAmI>;
|
|
50
|
+
/**
|
|
51
|
+
* Best-effort fill of the session's hosted avatar (PHNX-3547): sessions written
|
|
52
|
+
* before the CLI tracked avatars have none, so ask `/api/v1/auth/me` and merge
|
|
53
|
+
* a hosted `avatar_url` into the persisted session — future publishes then
|
|
54
|
+
* stamp the OAuth profile image instead of only a Gravatar hash. A no-op when
|
|
55
|
+
* signed out, when an avatar is already stored, or when the server exposes
|
|
56
|
+
* none; network/server failures are swallowed (the share attribution falls
|
|
57
|
+
* back to Gravatar/initials either way).
|
|
58
|
+
*/
|
|
59
|
+
export declare function refreshSessionAvatar(): Promise<void>;
|
|
44
60
|
export interface SpaceSummary {
|
|
45
61
|
id: string;
|
|
46
62
|
slug: string;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Phoenix ID — the typed surface commands use. Every route the account backend
|
|
3
3
|
* exposes is a function here; no command builds a URL or reads a token itself.
|
|
4
4
|
*/
|
|
5
|
-
import { phoenixRequest, PhoenixApiError } from './client.js';
|
|
5
|
+
import { phoenixRequest, PhoenixApiError, readSession, writeSession } from './client.js';
|
|
6
6
|
export { PHOENIX_ID_BASE, PhoenixApiError, clearSession, readSession, sessionFilePath, writeSession, } from './client.js';
|
|
7
7
|
export function startDeviceAuthorization() {
|
|
8
8
|
return phoenixRequest('POST', '/api/v1/auth/device/authorization', {
|
|
@@ -38,6 +38,30 @@ export async function pollDeviceToken(deviceCode) {
|
|
|
38
38
|
export function fetchWhoAmI(token) {
|
|
39
39
|
return phoenixRequest('GET', '/api/v1/auth/me', { token });
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Best-effort fill of the session's hosted avatar (PHNX-3547): sessions written
|
|
43
|
+
* before the CLI tracked avatars have none, so ask `/api/v1/auth/me` and merge
|
|
44
|
+
* a hosted `avatar_url` into the persisted session — future publishes then
|
|
45
|
+
* stamp the OAuth profile image instead of only a Gravatar hash. A no-op when
|
|
46
|
+
* signed out, when an avatar is already stored, or when the server exposes
|
|
47
|
+
* none; network/server failures are swallowed (the share attribution falls
|
|
48
|
+
* back to Gravatar/initials either way).
|
|
49
|
+
*/
|
|
50
|
+
export async function refreshSessionAvatar() {
|
|
51
|
+
const session = readSession();
|
|
52
|
+
if (!session || session.avatarUrl)
|
|
53
|
+
return;
|
|
54
|
+
try {
|
|
55
|
+
const me = await fetchWhoAmI();
|
|
56
|
+
const hosted = me.avatar_url?.trim();
|
|
57
|
+
if (hosted && /^https:\/\//i.test(hosted)) {
|
|
58
|
+
writeSession({ ...session, avatarUrl: hosted });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Offline or server without the field — the Gravatar fallback covers it.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
41
65
|
export const listSpaces = () => phoenixRequest('GET', '/api/v1/spaces');
|
|
42
66
|
export const createSpace = (input) => phoenixRequest('POST', '/api/v1/spaces', { body: input });
|
|
43
67
|
export const getSpace = (id) => phoenixRequest('GET', `/api/v1/spaces/${encodeURIComponent(id)}`);
|
package/dist/lib/profiles.d.ts
CHANGED
|
@@ -177,6 +177,8 @@ export interface HostModelOptions {
|
|
|
177
177
|
export declare function profileFromHostModel(name: string, host: AgentId, model: string, opts?: HostModelOptions): Profile;
|
|
178
178
|
/** Overrides applied on top of the source when forking a harness. */
|
|
179
179
|
export interface ForkProfileOptions {
|
|
180
|
+
/** Translate the fork onto a different native harness host. */
|
|
181
|
+
host?: AgentId;
|
|
180
182
|
/** Swap the pinned model. Written onto the source's model env key when it has
|
|
181
183
|
* one, else onto the host's canonical model var. */
|
|
182
184
|
model?: string;
|
package/dist/lib/profiles.js
CHANGED
|
@@ -404,31 +404,50 @@ export function profileFromHostModel(name, host, model, opts = {}) {
|
|
|
404
404
|
*/
|
|
405
405
|
export function forkProfile(source, name, opts = {}) {
|
|
406
406
|
validateProfileName(name);
|
|
407
|
-
const
|
|
407
|
+
const sourceHost = source.host.agent;
|
|
408
|
+
const host = opts.host ?? sourceHost;
|
|
408
409
|
const env = { ...source.env };
|
|
409
|
-
|
|
410
|
-
|
|
410
|
+
const sourceModelKey = profileModelEnvKey(source) ?? modelEnvKeyForHost(sourceHost);
|
|
411
|
+
const targetModelKey = modelEnvKeyForHost(host);
|
|
412
|
+
const model = opts.model ?? env[sourceModelKey];
|
|
413
|
+
if (host !== sourceHost && sourceModelKey !== targetModelKey)
|
|
414
|
+
delete env[sourceModelKey];
|
|
415
|
+
if (model) {
|
|
416
|
+
env[targetModelKey] = model;
|
|
411
417
|
}
|
|
412
|
-
|
|
413
|
-
|
|
418
|
+
const sourceBaseKey = baseUrlEnvKeyForHost(sourceHost);
|
|
419
|
+
const targetBaseKey = baseUrlEnvKeyForHost(host);
|
|
420
|
+
const baseUrl = opts.baseUrl ?? (sourceBaseKey ? env[sourceBaseKey] : undefined);
|
|
421
|
+
if (host !== sourceHost && sourceBaseKey && sourceBaseKey !== targetBaseKey)
|
|
422
|
+
delete env[sourceBaseKey];
|
|
423
|
+
if (baseUrl) {
|
|
424
|
+
const key = targetBaseKey;
|
|
414
425
|
if (!key) {
|
|
415
426
|
throw new Error(`Host '${host}' has no known base-URL env var; drop --base-url or fork onto a claude/codex host.`);
|
|
416
427
|
}
|
|
417
|
-
env[key] =
|
|
428
|
+
env[key] = baseUrl;
|
|
418
429
|
}
|
|
419
430
|
const forked = {
|
|
420
431
|
...source,
|
|
421
432
|
name,
|
|
422
|
-
host: { agent: host, ...(opts.version ? { version: opts.version } : source.host.version ? { version: source.host.version } : {}) },
|
|
433
|
+
host: { agent: host, ...(opts.version ? { version: opts.version } : host === sourceHost && source.host.version ? { version: source.host.version } : {}) },
|
|
423
434
|
env,
|
|
424
435
|
// The source's description names the source's model, so inheriting it
|
|
425
436
|
// across a model swap would describe the fork wrongly.
|
|
426
|
-
description: opts.description ?? (opts.model
|
|
437
|
+
description: opts.description ?? (opts.model || host !== sourceHost
|
|
438
|
+
? `Forked from ${source.name}: ${model ?? host}`
|
|
439
|
+
: source.description),
|
|
427
440
|
forkedFrom: source.name,
|
|
428
441
|
};
|
|
442
|
+
if (forked.auth && host !== sourceHost) {
|
|
443
|
+
const envVar = authEnvKeyForHost(host);
|
|
444
|
+
if (!envVar)
|
|
445
|
+
throw new Error(`Host '${host}' has no known auth env var; the source auth binding cannot be translated.`);
|
|
446
|
+
forked.auth = { ...forked.auth, envVar };
|
|
447
|
+
}
|
|
429
448
|
// A fork that repoints the model or endpoint is no longer that preset — keep
|
|
430
449
|
// the preset link only while the fork still matches what the preset defines.
|
|
431
|
-
if (opts.model || opts.baseUrl)
|
|
450
|
+
if (opts.model || opts.baseUrl || host !== sourceHost)
|
|
432
451
|
delete forked.preset;
|
|
433
452
|
if (opts.provider) {
|
|
434
453
|
const envVar = opts.authEnvVar ?? source.auth?.envVar ?? authEnvKeyForHost(host);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-reconcile repair — the shared "make it whole" pass `agents sync` runs
|
|
3
|
+
* after its prune+write, so sync is a true superset of the old `doctor --fix`.
|
|
4
|
+
*
|
|
5
|
+
* `syncResourcesToVersion` reconciles resource FILES (and, with a caller
|
|
6
|
+
* selection, prunes orphans). It does NOT: fill live-home gaps the staleness
|
|
7
|
+
* manifest can't see, generate/repair the managed hook runtime shims, or re-wire
|
|
8
|
+
* a present-but-unwired hook into settings.json. `doctor --fix` used to do all of
|
|
9
|
+
* that; now sync does, by calling this one routine at the tail of every reconcile.
|
|
10
|
+
* (The destructive stale-CLI purge `doctor --fix` also ran is NOT automatic here
|
|
11
|
+
* — it runs only via the explicit `agents sync --prune-clis`, see `pruneClis`.)
|
|
12
|
+
*
|
|
13
|
+
* Ordering matters: sync's own prune+write happens FIRST (unchanged), THEN this
|
|
14
|
+
* repair pass. Prune stays sync's job; heal only FILLS and FIXES (never deletes),
|
|
15
|
+
* so the two never fight. Pure of stdout — returns a structured report;
|
|
16
|
+
* `renderRepairAfterSync` prints it only when the caller is neither `--json` nor
|
|
17
|
+
* `--quiet`.
|
|
18
|
+
*/
|
|
19
|
+
import type { AgentId } from './types.js';
|
|
20
|
+
import { type HealResult } from './heal.js';
|
|
21
|
+
import { type HookRuntimeRepairReport } from './hooks/install.js';
|
|
22
|
+
import { type RemediateStaleInstallsResult, type FindAgentsCliInstallsOptions } from './self-update.js';
|
|
23
|
+
export interface HookRewireResult {
|
|
24
|
+
agent: AgentId;
|
|
25
|
+
version: string;
|
|
26
|
+
/** Hooks newly wired into settings.json by this pass. */
|
|
27
|
+
rewired: number;
|
|
28
|
+
/** Hooks still unwired after re-registering (source/home mismatch). */
|
|
29
|
+
remaining: number;
|
|
30
|
+
/** Stable failure class; never expose a generated temporary shim pathname. */
|
|
31
|
+
failure?: 'register-failed';
|
|
32
|
+
}
|
|
33
|
+
export interface RepairAfterSyncReport {
|
|
34
|
+
heal: HealResult;
|
|
35
|
+
hookRewire: HookRewireResult[];
|
|
36
|
+
hookRuntimeRepair: HookRuntimeRepairReport;
|
|
37
|
+
/** Only populated on the umbrella (no-agent) path; null otherwise. */
|
|
38
|
+
staleInstallPurge: RemediateStaleInstallsResult | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Test/override hook that scopes the destructive stale-CLI purge to injected
|
|
42
|
+
* paths so it NEVER touches real machine installs. `findAgentsCliInstalls`
|
|
43
|
+
* hard-codes `/usr/local/lib/node_modules` et al. when `globalNodeModulesDirs` is
|
|
44
|
+
* absent, so a purge is unsafe to run unscoped in a test — inject sandbox paths.
|
|
45
|
+
*/
|
|
46
|
+
export interface PurgeInjection {
|
|
47
|
+
runningRoot?: string;
|
|
48
|
+
runningVersion?: string;
|
|
49
|
+
pathEnv?: string;
|
|
50
|
+
findOpts?: FindAgentsCliInstallsOptions;
|
|
51
|
+
dryRun?: boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface RepairAfterSyncOptions {
|
|
54
|
+
/** Scope the repair to one agent; omit for the umbrella sweep across all
|
|
55
|
+
* installed agents. (The purge is NOT tied to this — see `pruneClis`.) */
|
|
56
|
+
agent?: AgentId;
|
|
57
|
+
/** The versions of `agent` the sync just reconciled. Passing them scopes
|
|
58
|
+
* heal + rewire + runtime-repair to exactly those homes; omitting lets heal
|
|
59
|
+
* apply its own non-isolated sweep. */
|
|
60
|
+
versions?: string[];
|
|
61
|
+
/** Resolution cwd for heal's diff. Defaults to heal's neutral home dir. */
|
|
62
|
+
cwd?: string;
|
|
63
|
+
/** Run the DESTRUCTIVE machine-wide stale-CLI purge (`fs.rmSync`s other
|
|
64
|
+
* agents-cli installs). Default false — the purge is NEVER automatic; it runs
|
|
65
|
+
* only on the umbrella (no-agent) path AND only when the caller passed
|
|
66
|
+
* `agents sync --prune-clis`. */
|
|
67
|
+
pruneClis?: boolean;
|
|
68
|
+
/** Scope the purge scan+delete to injected paths (tests only). */
|
|
69
|
+
purgeInjection?: PurgeInjection;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Run the post-reconcile repair pass over the agents/versions a sync just
|
|
73
|
+
* touched: heal live-home gaps (mode 'full', matching the old `doctor --fix`),
|
|
74
|
+
* re-wire hooks the diff-driven heal leaves behind, run one bounded managed-hook
|
|
75
|
+
* runtime repair, and — ONLY when `pruneClis` is set on the umbrella (no-agent)
|
|
76
|
+
* path — purge stale/legacy agents-cli copies. The purge is never automatic.
|
|
77
|
+
* Returns a full account; never writes to stdout.
|
|
78
|
+
*/
|
|
79
|
+
export declare function repairAfterSync(opts: RepairAfterSyncOptions): Promise<RepairAfterSyncReport>;
|
|
80
|
+
/** True when the repair pass changed (or attempted to change) anything. */
|
|
81
|
+
export declare function repairChangedAnything(report: RepairAfterSyncReport): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* True when the RECONCILE repair left a per-version problem a human must fix — an
|
|
84
|
+
* unresolvable managed hook runtime shim, or a hook that could not be re-wired.
|
|
85
|
+
* These gate a sync's `ok`/exit code (the yosemite-s1 class the old
|
|
86
|
+
* `doctor --fix` surfaced).
|
|
87
|
+
*
|
|
88
|
+
* The machine-wide stale-CLI purge is deliberately EXCLUDED: a purge that could
|
|
89
|
+
* not delete a system-wide install (typically `EACCES` on `/usr/local/...`, which
|
|
90
|
+
* needs sudo) is a hygiene issue unrelated to whether the reconcile succeeded, so
|
|
91
|
+
* it must not flip a fleet peer's `agents sync --json` to `ok: false`. Purge
|
|
92
|
+
* failures stay fully visible — in the JSON `repair.staleInstallPurge.failed` and
|
|
93
|
+
* the rendered `hold`/`manual` lines.
|
|
94
|
+
*/
|
|
95
|
+
export declare function repairHadFailures(report: RepairAfterSyncReport): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Serialize the repair pass for a `--json` sync payload — the machine surface the
|
|
98
|
+
* deleted `doctor --fix --json` used to carry (heal detail + hook rewire + hook
|
|
99
|
+
* runtime repair + the umbrella stale-CLI purge). Attached under the `repair` key
|
|
100
|
+
* so fleet fan-out sees what the reconcile fixed and whether anything still needs
|
|
101
|
+
* a human (mirror `repairHadFailures`).
|
|
102
|
+
*/
|
|
103
|
+
export declare function repairAfterSyncJson(report: RepairAfterSyncReport): Record<string, unknown>;
|
|
104
|
+
/**
|
|
105
|
+
* Print the repair pass's human-readable detail. Sync calls this only on the
|
|
106
|
+
* interactive / non-json path; the header is emitted only when something actually
|
|
107
|
+
* changed, so a clean sync stays quiet.
|
|
108
|
+
*/
|
|
109
|
+
export declare function renderRepairAfterSync(report: RepairAfterSyncReport, log?: (s: string) => void): void;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { AGENTS, ALL_AGENT_IDS } from './agents.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { heal, healChangedAnything } from './heal.js';
|
|
4
|
+
import { checkVersionHookWiring, registerHooksToSettings, repairManagedHookRuntimeArtifacts, } from './hooks/install.js';
|
|
5
|
+
import { getVersionHomePath, listInstalledVersions, isVersionIsolated, } from './installations/versions.js';
|
|
6
|
+
import { invalidateDoctorOverviewCache } from './devices/doctor-overview-cache.js';
|
|
7
|
+
import { remediateStaleAgentsCliInstalls, resolveRunningPackageRoot, } from './self-update.js';
|
|
8
|
+
import { getCliVersion } from './version.js';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
const __moduleDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
|
|
13
|
+
// Claude-family agents whose native hook wiring `checkVersionHookWiring` can
|
|
14
|
+
// verify (and `registerHooksToSettings` re-wire).
|
|
15
|
+
const HOOK_WIRING_FIX_AGENTS = ['claude', 'droid'];
|
|
16
|
+
// ─── hook re-wire (files reconciled but not referenced in settings.json) ─────
|
|
17
|
+
/**
|
|
18
|
+
* Re-wire hooks that reconcile as files but are absent from settings.json.
|
|
19
|
+
*
|
|
20
|
+
* heal() only re-syncs resources the diff flags missing/diff; a hook whose file
|
|
21
|
+
* is byte-identical to source but never referenced in settings.json is neither,
|
|
22
|
+
* so heal walks past it. registerHooksToSettings (the same call `agents sync`
|
|
23
|
+
* makes at versions.ts) regenerates the wiring, so run it for any Claude-family
|
|
24
|
+
* version this repair targets that has unwired hooks. Only claude/droid — the
|
|
25
|
+
* set checkVersionHookWiring can verify.
|
|
26
|
+
*/
|
|
27
|
+
function rewireUnwiredHooks(agent, versions) {
|
|
28
|
+
const out = [];
|
|
29
|
+
const agents = agent
|
|
30
|
+
? (HOOK_WIRING_FIX_AGENTS.includes(agent) ? [agent] : [])
|
|
31
|
+
: HOOK_WIRING_FIX_AGENTS;
|
|
32
|
+
for (const a of agents) {
|
|
33
|
+
// Explicit versions (the sync's reconcile targets) are honoured as-is; a
|
|
34
|
+
// sweep excludes isolated copies, mirroring heal().
|
|
35
|
+
const vers = agent && versions && versions.length > 0
|
|
36
|
+
? versions
|
|
37
|
+
: listInstalledVersions(a).filter((v) => !isVersionIsolated(a, v));
|
|
38
|
+
for (const version of vers) {
|
|
39
|
+
const before = checkVersionHookWiring(a, version);
|
|
40
|
+
if (!before.supported)
|
|
41
|
+
continue;
|
|
42
|
+
const need = before.unwired.length + (before.settingsMissing ? (before.expected ?? 0) : 0);
|
|
43
|
+
if (need === 0)
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
const registration = registerHooksToSettings(a, getVersionHomePath(a, version));
|
|
47
|
+
if (registration.errors.length > 0) {
|
|
48
|
+
out.push({ agent: a, version, rewired: 0, remaining: need, failure: 'register-failed' });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const after = checkVersionHookWiring(a, version);
|
|
52
|
+
const remaining = after.unwired.length + (after.settingsMissing ? (after.expected ?? 0) : 0);
|
|
53
|
+
out.push({ agent: a, version, rewired: Math.max(0, need - remaining), remaining });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// A shim write can fail before the native config writer returns its own
|
|
57
|
+
// errors. Record the same stable class and keep the repair moving to the
|
|
58
|
+
// one bounded runtime repair pass below.
|
|
59
|
+
out.push({ agent: a, version, rewired: 0, remaining: need, failure: 'register-failed' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function runtimeRepairFilter(agent, versions) {
|
|
66
|
+
if (!agent)
|
|
67
|
+
return undefined;
|
|
68
|
+
// A single concrete target narrows to that version. Broad selectors (@all,
|
|
69
|
+
// agent-only) still make one bounded repair pass for that harness.
|
|
70
|
+
return {
|
|
71
|
+
agent,
|
|
72
|
+
...(versions && versions.length === 1 ? { version: versions[0] } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* RUSH-2415: delete npx-cache / unsafe-legacy / pre-1.22.30 agents-cli copies
|
|
77
|
+
* when a fixed peer already exists. DESTRUCTIVE (`fs.rmSync`), so it is never
|
|
78
|
+
* automatic — it runs only via `agents sync --prune-clis`. `injection` scopes the
|
|
79
|
+
* scan+delete to sandbox paths in tests so it can never touch a real install.
|
|
80
|
+
*/
|
|
81
|
+
function purgeStaleAgentsCliCopies(injection) {
|
|
82
|
+
let runningRoot = injection?.runningRoot;
|
|
83
|
+
if (!runningRoot) {
|
|
84
|
+
try {
|
|
85
|
+
// resolveRunningPackageRoot walks up until package.json names this package.
|
|
86
|
+
runningRoot = resolveRunningPackageRoot(__moduleDirname);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return remediateStaleAgentsCliInstalls({
|
|
93
|
+
runningRoot,
|
|
94
|
+
runningVersion: injection?.runningVersion ?? getCliVersion(),
|
|
95
|
+
...(injection?.pathEnv !== undefined ? { pathEnv: injection.pathEnv } : {}),
|
|
96
|
+
...(injection?.findOpts ? { findOpts: injection.findOpts } : {}),
|
|
97
|
+
...(injection?.dryRun ? { dryRun: injection.dryRun } : {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// ─── public entrypoint ────────────────────────────────────────────────────────
|
|
101
|
+
/**
|
|
102
|
+
* Run the post-reconcile repair pass over the agents/versions a sync just
|
|
103
|
+
* touched: heal live-home gaps (mode 'full', matching the old `doctor --fix`),
|
|
104
|
+
* re-wire hooks the diff-driven heal leaves behind, run one bounded managed-hook
|
|
105
|
+
* runtime repair, and — ONLY when `pruneClis` is set on the umbrella (no-agent)
|
|
106
|
+
* path — purge stale/legacy agents-cli copies. The purge is never automatic.
|
|
107
|
+
* Returns a full account; never writes to stdout.
|
|
108
|
+
*/
|
|
109
|
+
export async function repairAfterSync(opts) {
|
|
110
|
+
const healResult = await heal({
|
|
111
|
+
mode: 'full',
|
|
112
|
+
agent: opts.agent,
|
|
113
|
+
versions: opts.agent ? opts.versions : undefined,
|
|
114
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
115
|
+
});
|
|
116
|
+
// Re-wire hooks the diff-driven heal leaves behind (present file, not wired).
|
|
117
|
+
const hookRewire = rewireUnwiredHooks(opts.agent, opts.versions);
|
|
118
|
+
// One inspect→generate→verify pass after resource + native-wiring repair have
|
|
119
|
+
// settled. This routine never calls sync/register and never retries.
|
|
120
|
+
const hookRuntimeRepair = repairManagedHookRuntimeArtifacts({
|
|
121
|
+
filter: runtimeRepairFilter(opts.agent, opts.versions),
|
|
122
|
+
});
|
|
123
|
+
// The stale-CLI purge is DESTRUCTIVE and NEVER automatic: it runs only on the
|
|
124
|
+
// umbrella (no-agent) path AND only when the caller opted in via
|
|
125
|
+
// `agents sync --prune-clis`. Drift-fixers above always run; this does not.
|
|
126
|
+
const staleInstallPurge = (!opts.agent && opts.pruneClis === true)
|
|
127
|
+
? purgeStaleAgentsCliCopies(opts.purgeInjection)
|
|
128
|
+
: null;
|
|
129
|
+
const report = {
|
|
130
|
+
heal: healResult,
|
|
131
|
+
hookRewire,
|
|
132
|
+
hookRuntimeRepair,
|
|
133
|
+
staleInstallPurge,
|
|
134
|
+
};
|
|
135
|
+
if (repairChangedAnything(report))
|
|
136
|
+
invalidateDoctorOverviewCache();
|
|
137
|
+
return report;
|
|
138
|
+
}
|
|
139
|
+
/** True when the repair pass changed (or attempted to change) anything. */
|
|
140
|
+
export function repairChangedAnything(report) {
|
|
141
|
+
return (healChangedAnything(report.heal) ||
|
|
142
|
+
report.hookRewire.some((r) => r.rewired > 0 || r.remaining > 0 || r.failure !== undefined) ||
|
|
143
|
+
report.hookRuntimeRepair.attempts.length > 0 ||
|
|
144
|
+
(report.staleInstallPurge !== null &&
|
|
145
|
+
(report.staleInstallPurge.removed.length > 0 || report.staleInstallPurge.failed.length > 0)));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* True when the RECONCILE repair left a per-version problem a human must fix — an
|
|
149
|
+
* unresolvable managed hook runtime shim, or a hook that could not be re-wired.
|
|
150
|
+
* These gate a sync's `ok`/exit code (the yosemite-s1 class the old
|
|
151
|
+
* `doctor --fix` surfaced).
|
|
152
|
+
*
|
|
153
|
+
* The machine-wide stale-CLI purge is deliberately EXCLUDED: a purge that could
|
|
154
|
+
* not delete a system-wide install (typically `EACCES` on `/usr/local/...`, which
|
|
155
|
+
* needs sudo) is a hygiene issue unrelated to whether the reconcile succeeded, so
|
|
156
|
+
* it must not flip a fleet peer's `agents sync --json` to `ok: false`. Purge
|
|
157
|
+
* failures stay fully visible — in the JSON `repair.staleInstallPurge.failed` and
|
|
158
|
+
* the rendered `hold`/`manual` lines.
|
|
159
|
+
*/
|
|
160
|
+
export function repairHadFailures(report) {
|
|
161
|
+
return (report.hookRewire.some((r) => r.failure !== undefined) ||
|
|
162
|
+
report.hookRuntimeRepair.needsAttention.length > 0);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Serialize the repair pass for a `--json` sync payload — the machine surface the
|
|
166
|
+
* deleted `doctor --fix --json` used to carry (heal detail + hook rewire + hook
|
|
167
|
+
* runtime repair + the umbrella stale-CLI purge). Attached under the `repair` key
|
|
168
|
+
* so fleet fan-out sees what the reconcile fixed and whether anything still needs
|
|
169
|
+
* a human (mirror `repairHadFailures`).
|
|
170
|
+
*/
|
|
171
|
+
export function repairAfterSyncJson(report) {
|
|
172
|
+
return {
|
|
173
|
+
heal: report.heal,
|
|
174
|
+
hookRewire: report.hookRewire,
|
|
175
|
+
hookRuntimeRepair: report.hookRuntimeRepair,
|
|
176
|
+
staleInstallPurge: report.staleInstallPurge,
|
|
177
|
+
hadFailures: repairHadFailures(report),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// ─── rendering (human output; sync calls this only when !json && !quiet) ─────
|
|
181
|
+
function renderHealText(result, log) {
|
|
182
|
+
for (const r of result.repairedManifests) {
|
|
183
|
+
log(` ${chalk.green('repair')} plugin ${chalk.bold(r.plugin)} ${chalk.gray(`— dropped invalid ${r.droppedFields.join(', ')} field`)}`);
|
|
184
|
+
}
|
|
185
|
+
for (const r of result.refreshedPlugins) {
|
|
186
|
+
log(` ${chalk.green('refresh')} plugin ${chalk.bold(r.plugin)} ${chalk.gray(`${r.from} → ${r.to}`)}`);
|
|
187
|
+
}
|
|
188
|
+
for (const s of result.skippedPlugins) {
|
|
189
|
+
const why = s.reason === 'modified'
|
|
190
|
+
? `locally modified — left as-is (run \`agents plugins update ${s.plugin}\` to force)`
|
|
191
|
+
: `no baseline recorded — left as-is (run \`agents plugins update ${s.plugin}\` to adopt)`;
|
|
192
|
+
log(` ${chalk.yellow('hold ')} plugin ${chalk.bold(s.plugin)} ${chalk.gray(`${s.from} → ${s.upstream} available; ${why}`)}`);
|
|
193
|
+
}
|
|
194
|
+
for (const v of result.versions) {
|
|
195
|
+
const label = `${AGENT_NAMES[v.agent] || v.agent}@${v.version}`;
|
|
196
|
+
if (v.healed.length === 0 && v.skipped.length === 0)
|
|
197
|
+
continue;
|
|
198
|
+
const byKind = new Map();
|
|
199
|
+
for (const h of v.healed)
|
|
200
|
+
byKind.set(h.kind, (byKind.get(h.kind) ?? 0) + 1);
|
|
201
|
+
const parts = Array.from(byKind, ([k, n]) => `${n} ${k}`);
|
|
202
|
+
if (v.healed.length > 0) {
|
|
203
|
+
log(` ${chalk.green('fixed ')} ${label} ${chalk.gray(parts.join(', '))}`);
|
|
204
|
+
}
|
|
205
|
+
const drift = v.skipped.filter((s) => s.reason === 'drift');
|
|
206
|
+
const unres = v.skipped.filter((s) => s.reason === 'unreconcilable');
|
|
207
|
+
if (drift.length > 0) {
|
|
208
|
+
log(` ${chalk.yellow('drift ')} ${label} ${chalk.gray(`${drift.length} hand-edited — left as-is (use \`--diff\` to inspect)`)}`);
|
|
209
|
+
}
|
|
210
|
+
if (unres.length > 0) {
|
|
211
|
+
const names = unres.map((s) => `${s.kind}/${s.name}`).join(', ');
|
|
212
|
+
log(` ${chalk.yellow('hold ')} ${label} ${chalk.gray(`${unres.length} couldn't reconcile (${names}) — source/home mismatch the writer can't satisfy`)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function renderHookRewireText(rewired, log) {
|
|
217
|
+
for (const r of rewired) {
|
|
218
|
+
const label = `${AGENT_NAMES[r.agent] || r.agent}@${r.version}`;
|
|
219
|
+
if (r.failure) {
|
|
220
|
+
log(` ${chalk.red('hold ')} ${label} ${chalk.gray('native hook wiring could not be updated')}`);
|
|
221
|
+
}
|
|
222
|
+
else if (r.remaining === 0) {
|
|
223
|
+
log(` ${chalk.green('rewired')} ${label} ${chalk.gray(`${r.rewired} hook${r.rewired === 1 ? '' : 's'} wired into settings.json`)}`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
log(` ${chalk.yellow('hold ')} ${label} ${chalk.gray(`${r.remaining} hook${r.remaining === 1 ? '' : 's'} still unwired — run \`agents sync ${r.agent}@${r.version} --yes\``)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function renderHookRuntimeRepairText(repair, log) {
|
|
231
|
+
for (const fixed of repair.fixed) {
|
|
232
|
+
log(` ${chalk.green('fixed ')} ${chalk.gray(fixed)}`);
|
|
233
|
+
}
|
|
234
|
+
for (const unresolved of repair.needsAttention) {
|
|
235
|
+
log(` ${chalk.red('hold ')} ${chalk.gray(unresolved)}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function renderStaleInstallPurgeText(purge, log) {
|
|
239
|
+
if (purge.removed.length === 0 && purge.failed.length === 0 && purge.unresolved.length === 0)
|
|
240
|
+
return;
|
|
241
|
+
log(chalk.bold('\nStale agents-cli installs'));
|
|
242
|
+
for (const r of purge.removed) {
|
|
243
|
+
const why = r.reasons.join(', ');
|
|
244
|
+
log(` ${chalk.green('purged')} ${chalk.gray(`${r.packageRoot} ${r.version} (${why})`)}`);
|
|
245
|
+
}
|
|
246
|
+
for (const f of purge.failed) {
|
|
247
|
+
log(` ${chalk.red('hold ')} ${chalk.gray(`${f.packageRoot} ${f.version} — ${f.error}`)}`);
|
|
248
|
+
}
|
|
249
|
+
for (const u of purge.unresolved) {
|
|
250
|
+
log(` ${chalk.yellow('manual')} ${chalk.gray(`${u.packageRoot} ${u.version} — remove it with:`)}`);
|
|
251
|
+
log(` ${chalk.bold(u.manualRemoveCommand)}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Print the repair pass's human-readable detail. Sync calls this only on the
|
|
256
|
+
* interactive / non-json path; the header is emitted only when something actually
|
|
257
|
+
* changed, so a clean sync stays quiet.
|
|
258
|
+
*/
|
|
259
|
+
export function renderRepairAfterSync(report, log = (s) => console.log(s)) {
|
|
260
|
+
if (!repairChangedAnything(report))
|
|
261
|
+
return;
|
|
262
|
+
renderHealText(report.heal, log);
|
|
263
|
+
renderHookRewireText(report.hookRewire, log);
|
|
264
|
+
renderHookRuntimeRepairText(report.hookRuntimeRepair, log);
|
|
265
|
+
if (report.staleInstallPurge)
|
|
266
|
+
renderStaleInstallPurgeText(report.staleInstallPurge, log);
|
|
267
|
+
}
|
package/dist/lib/routers.d.ts
CHANGED
|
@@ -52,6 +52,17 @@ export declare function readRouter(name: string, cwd?: string): Router;
|
|
|
52
52
|
export declare function writeRouter(router: Router): void;
|
|
53
53
|
/** Delete a router from the user layer. Returns false if it did not exist there. */
|
|
54
54
|
export declare function deleteRouter(name: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Rename a router on disk, re-keying the user-layer file and preserving every
|
|
57
|
+
* field (harness allowlists, weights, linked accounts, hijack flag).
|
|
58
|
+
*
|
|
59
|
+
* Throws if `oldName` does not resolve, if it resolves from a non-user layer
|
|
60
|
+
* (renaming would write a shadowed user-layer copy -- see {@link routerSource}),
|
|
61
|
+
* or if `newName` already resolves in any layer. There is no `--force` /
|
|
62
|
+
* overwrite path -- a collision is a hard error directing the user to remove
|
|
63
|
+
* the target first.
|
|
64
|
+
*/
|
|
65
|
+
export declare function renameRouter(oldName: string, newName: string): void;
|
|
55
66
|
/**
|
|
56
67
|
* Fail-loud token validation (E1 of the Agent Router spec): a router MUST NOT
|
|
57
68
|
* persist a harness id or model/tier token this machine cannot vouch for.
|
|
@@ -76,6 +87,6 @@ export declare function validateRouter(router: Router): void;
|
|
|
76
87
|
/**
|
|
77
88
|
* List every router resolved project > user > system (deduplicated union,
|
|
78
89
|
* project wins on a name collision -- same precedence as {@link resolveResource}).
|
|
79
|
-
* Malformed files are silently skipped, surfaced via `agents route
|
|
90
|
+
* Malformed files are silently skipped, surfaced via `agents route view <name>`.
|
|
80
91
|
*/
|
|
81
92
|
export declare function listRouters(cwd?: string): Router[];
|
package/dist/lib/routers.js
CHANGED
|
@@ -89,6 +89,35 @@ export function deleteRouter(name) {
|
|
|
89
89
|
fs.unlinkSync(file);
|
|
90
90
|
return true;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Rename a router on disk, re-keying the user-layer file and preserving every
|
|
94
|
+
* field (harness allowlists, weights, linked accounts, hijack flag).
|
|
95
|
+
*
|
|
96
|
+
* Throws if `oldName` does not resolve, if it resolves from a non-user layer
|
|
97
|
+
* (renaming would write a shadowed user-layer copy -- see {@link routerSource}),
|
|
98
|
+
* or if `newName` already resolves in any layer. There is no `--force` /
|
|
99
|
+
* overwrite path -- a collision is a hard error directing the user to remove
|
|
100
|
+
* the target first.
|
|
101
|
+
*/
|
|
102
|
+
export function renameRouter(oldName, newName) {
|
|
103
|
+
validateRouterName(newName);
|
|
104
|
+
const source = routerSource(oldName);
|
|
105
|
+
if (source === null) {
|
|
106
|
+
throw new Error(`Router '${oldName}' not found.`);
|
|
107
|
+
}
|
|
108
|
+
if (source !== 'user') {
|
|
109
|
+
throw new Error(`Router '${oldName}' resolves from the '${source}' layer, not 'user' -- ` +
|
|
110
|
+
`agents route rename can only rename a user-layer router. Rename its file directly, ` +
|
|
111
|
+
`or create a user-layer router under a different name.`);
|
|
112
|
+
}
|
|
113
|
+
if (routerExists(newName)) {
|
|
114
|
+
throw new Error(`Router '${newName}' already exists; remove it first.`);
|
|
115
|
+
}
|
|
116
|
+
const router = readRouter(oldName);
|
|
117
|
+
router.name = newName;
|
|
118
|
+
writeRouter(router);
|
|
119
|
+
deleteRouter(oldName);
|
|
120
|
+
}
|
|
92
121
|
/**
|
|
93
122
|
* Fail-loud token validation (E1 of the Agent Router spec): a router MUST NOT
|
|
94
123
|
* persist a harness id or model/tier token this machine cannot vouch for.
|
|
@@ -137,7 +166,7 @@ export function validateRouter(router) {
|
|
|
137
166
|
/**
|
|
138
167
|
* List every router resolved project > user > system (deduplicated union,
|
|
139
168
|
* project wins on a name collision -- same precedence as {@link resolveResource}).
|
|
140
|
-
* Malformed files are silently skipped, surfaced via `agents route
|
|
169
|
+
* Malformed files are silently skipped, surfaced via `agents route view <name>`.
|
|
141
170
|
*/
|
|
142
171
|
export function listRouters(cwd) {
|
|
143
172
|
const routers = [];
|
|
@@ -1025,10 +1025,16 @@ export function validateJob(config) {
|
|
|
1025
1025
|
if (config.remoteCwd !== undefined && strategy !== 'host' && strategy !== 'fleet') {
|
|
1026
1026
|
errors.push('remoteCwd only applies to host/fleet-placed routines — set hostStrategy: host|fleet, or drop it');
|
|
1027
1027
|
}
|
|
1028
|
-
if (config.project
|
|
1028
|
+
if (config.project === null) {
|
|
1029
|
+
errors.push('project is null — quote YAML values that look like null literals');
|
|
1030
|
+
}
|
|
1031
|
+
else if (config.project !== undefined && (typeof config.project !== 'string' || config.project.trim() === '')) {
|
|
1029
1032
|
errors.push('project (the singular execution anchor) must be a non-empty project name');
|
|
1030
1033
|
}
|
|
1031
|
-
if (config.cwd
|
|
1034
|
+
if (config.cwd === null) {
|
|
1035
|
+
errors.push('cwd is null — a bare ~ is YAML null; quote it as "~" for the home directory');
|
|
1036
|
+
}
|
|
1037
|
+
else if (config.cwd !== undefined && (typeof config.cwd !== 'string' || config.cwd.trim() === '')) {
|
|
1032
1038
|
errors.push('cwd (the portable execution directory) must be a non-empty path string');
|
|
1033
1039
|
}
|
|
1034
1040
|
// `remoteCwd` is the legacy host-placement path; `cwd` is its canonical
|
|
@@ -432,6 +432,19 @@ export interface ActiveSession {
|
|
|
432
432
|
* session (PHNX-3688). Absent for non-tmux rows.
|
|
433
433
|
*/
|
|
434
434
|
tmuxName?: string;
|
|
435
|
+
/**
|
|
436
|
+
* Daemon-computed 1–2 line goal from the session's first user turn (PHNX-3939).
|
|
437
|
+
* Produced off the request path by the background `SessionSummarizerService`
|
|
438
|
+
* and merged from the `session_summaries` cache in {@link applyImmutableMemo};
|
|
439
|
+
* rides the `sessions watch` stream free via the `...row` spread.
|
|
440
|
+
*/
|
|
441
|
+
goal?: string;
|
|
442
|
+
/** Daemon-computed progress checkpoints, newest last (PHNX-3939). */
|
|
443
|
+
checkpoints?: import('./types.js').SessionCheckpoint[];
|
|
444
|
+
/** Daemon-computed detailed checklist for the session (PHNX-3939). */
|
|
445
|
+
summaryChecklist?: import('./types.js').SessionChecklistItem[];
|
|
446
|
+
/** Lifecycle of the daemon-computed summary; `skipped` when disabled (PHNX-3939). */
|
|
447
|
+
summaryState?: import('./types.js').SummaryState;
|
|
435
448
|
}
|
|
436
449
|
export declare function activeStatusFromCloudStatus(status: CloudTaskStatus): ActiveStatus;
|
|
437
450
|
interface ActiveQueryOptions {
|