@phnx-labs/agents-cli 1.22.60 → 1.22.62

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.
Files changed (98) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +6 -0
  3. package/dist/cli/command-registry.d.ts +1 -0
  4. package/dist/cli/command-registry.js +2 -0
  5. package/dist/commands/browser.js +9 -4
  6. package/dist/commands/doctor.js +1 -1
  7. package/dist/commands/exec.js +35 -1
  8. package/dist/commands/feed.js +3 -1
  9. package/dist/commands/harness-hooks.d.ts +55 -0
  10. package/dist/commands/harness-hooks.js +104 -0
  11. package/dist/commands/harness-wizard.d.ts +33 -14
  12. package/dist/commands/harness-wizard.js +53 -23
  13. package/dist/commands/harness.d.ts +14 -0
  14. package/dist/commands/harness.js +86 -5
  15. package/dist/commands/monitors.js +3 -3
  16. package/dist/commands/reminders.d.ts +9 -0
  17. package/dist/commands/reminders.js +49 -0
  18. package/dist/commands/run-account-picker.d.ts +14 -0
  19. package/dist/commands/run-account-picker.js +13 -0
  20. package/dist/commands/send.js +4 -1
  21. package/dist/commands/teams.d.ts +1 -1
  22. package/dist/commands/teams.js +9 -3
  23. package/dist/index.js +9 -0
  24. package/dist/lib/accounting/rotate.d.ts +63 -0
  25. package/dist/lib/accounting/rotate.js +229 -13
  26. package/dist/lib/browser/drivers/local.d.ts +11 -0
  27. package/dist/lib/browser/drivers/local.js +26 -0
  28. package/dist/lib/browser/profiles.js +8 -6
  29. package/dist/lib/browser/service.d.ts +12 -8
  30. package/dist/lib/browser/service.js +38 -10
  31. package/dist/lib/channels/owner-forward.d.ts +14 -8
  32. package/dist/lib/channels/owner-forward.js +9 -5
  33. package/dist/lib/channels/registry.d.ts +2 -0
  34. package/dist/lib/channels/send.js +13 -1
  35. package/dist/lib/claude-statusline.d.ts +14 -1
  36. package/dist/lib/claude-statusline.js +27 -2
  37. package/dist/lib/daemon/runner.js +17 -2
  38. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  39. package/dist/lib/devices/doctor-findings.js +22 -4
  40. package/dist/lib/doctor-diff.d.ts +21 -5
  41. package/dist/lib/doctor-diff.js +242 -76
  42. package/dist/lib/feed/events.d.ts +1 -1
  43. package/dist/lib/feed/events.js +25 -16
  44. package/dist/lib/feed-broadcast.js +5 -12
  45. package/dist/lib/github/gh-overload.d.ts +58 -0
  46. package/dist/lib/github/gh-overload.js +246 -0
  47. package/dist/lib/github/rest.d.ts +64 -0
  48. package/dist/lib/github/rest.js +111 -0
  49. package/dist/lib/harness-connection-test.d.ts +57 -0
  50. package/dist/lib/harness-connection-test.js +80 -0
  51. package/dist/lib/heal.js +8 -3
  52. package/dist/lib/humans.d.ts +9 -0
  53. package/dist/lib/humans.js +29 -8
  54. package/dist/lib/installations/shims.d.ts +22 -0
  55. package/dist/lib/installations/shims.js +104 -0
  56. package/dist/lib/linear-project-counts.js +8 -0
  57. package/dist/lib/linear-rate-limit.d.ts +26 -0
  58. package/dist/lib/linear-rate-limit.js +163 -0
  59. package/dist/lib/mcp.d.ts +9 -0
  60. package/dist/lib/mcp.js +37 -1
  61. package/dist/lib/notify.d.ts +3 -0
  62. package/dist/lib/notify.js +63 -21
  63. package/dist/lib/open-url.js +5 -3
  64. package/dist/lib/permissions.d.ts +28 -0
  65. package/dist/lib/permissions.js +156 -1
  66. package/dist/lib/refresh.js +9 -1
  67. package/dist/lib/reminders.d.ts +29 -0
  68. package/dist/lib/reminders.js +88 -0
  69. package/dist/lib/resource-content-diff.d.ts +33 -0
  70. package/dist/lib/resource-content-diff.js +103 -0
  71. package/dist/lib/rules/compile.d.ts +7 -0
  72. package/dist/lib/rules/compile.js +7 -1
  73. package/dist/lib/session/active.d.ts +41 -4
  74. package/dist/lib/session/active.js +58 -7
  75. package/dist/lib/session/host-link.d.ts +22 -0
  76. package/dist/lib/session/host-link.js +40 -4
  77. package/dist/lib/session/trajectory.d.ts +42 -0
  78. package/dist/lib/session/trajectory.js +46 -27
  79. package/dist/lib/ssh-exec.d.ts +30 -0
  80. package/dist/lib/ssh-exec.js +37 -5
  81. package/dist/lib/startup/command-registry.js +1 -1
  82. package/dist/lib/subagents-registry.d.ts +18 -0
  83. package/dist/lib/subagents-registry.js +79 -0
  84. package/dist/lib/teams/agents.d.ts +12 -0
  85. package/dist/lib/teams/agents.js +51 -0
  86. package/dist/lib/traces/schema2-build.d.ts +85 -0
  87. package/dist/lib/traces/schema2-build.js +637 -0
  88. package/dist/lib/traces/schema2-danger.d.ts +36 -0
  89. package/dist/lib/traces/schema2-danger.js +185 -0
  90. package/dist/lib/traces/schema2.d.ts +149 -0
  91. package/dist/lib/traces/schema2.js +20 -0
  92. package/dist/lib/traces/sync.d.ts +93 -0
  93. package/dist/lib/traces/sync.js +75 -22
  94. package/dist/lib/traces/worker-template.js +5 -0
  95. package/dist/lib/uninstall.js +10 -1
  96. package/dist/lib/workflows.d.ts +11 -0
  97. package/dist/lib/workflows.js +67 -8
  98. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { getOwnerNotifyFromHumans } from '../humans.js';
2
2
  import { registerBuiltinProviders } from './providers/index.js';
3
3
  import { resolveTransport } from './resolve.js';
4
+ import { sendToOwner } from '../notify.js';
4
5
  const OWNER_ALIAS = 'owner';
5
6
  /** True when the destination token means “the configured owner”. */
6
7
  export function isOwnerAlias(to) {
@@ -112,6 +113,17 @@ export async function sendMessage(input, meta) {
112
113
  const resolved = resolveSendEnvelope(input, meta);
113
114
  if (!resolved.ok)
114
115
  return { error: resolved.error };
115
- const result = await deliverEnvelope(resolved.envelope, meta);
116
+ const ownerPolicyRequest = resolved.envelope.ownerScoped === true
117
+ && !input.channel?.trim()
118
+ && (!input.to?.trim() || isOwnerAlias(input.to));
119
+ const result = ownerPolicyRequest
120
+ ? await sendToOwner(resolved.envelope.text, {
121
+ meta,
122
+ dryRun: resolved.envelope.dryRun,
123
+ thread: resolved.envelope.thread,
124
+ attachments: resolved.envelope.attachments,
125
+ from: resolved.envelope.from,
126
+ })
127
+ : await deliverEnvelope(resolved.envelope, meta);
116
128
  return { result, envelope: resolved.envelope };
117
129
  }
@@ -9,6 +9,7 @@ export declare const CLAUDE_STATUSLINE_COMMAND = "agents __claude-statusline";
9
9
  export declare function isStatusLineSelfReference(command: string): boolean;
10
10
  interface ClaudeStatusLinePayload {
11
11
  cwd?: string;
12
+ session_id?: string;
12
13
  workspace?: {
13
14
  current_dir?: string;
14
15
  };
@@ -29,7 +30,19 @@ interface ClaudeStatusLinePayload {
29
30
  }
30
31
  export declare function ingestClaudeStatusLineUsage(payload: ClaudeStatusLinePayload, versionHome: string): boolean;
31
32
  export declare function renderDelegate(payload: string, versionHome: string): string;
32
- export declare function renderClaudeStatusLine(payload: ClaudeStatusLinePayload, host?: string, delegated?: string): string;
33
+ /**
34
+ * Format a reminder as a dimmed statusline part (empty string when none). The
35
+ * ◆ marker distinguishes it from the host/model/usage parts, and the ANSI dim
36
+ * keeps it quiet next to the live figures.
37
+ */
38
+ export declare function formatReminderPart(short: string | undefined): string;
39
+ export declare function renderClaudeStatusLine(payload: ClaudeStatusLinePayload, host?: string, delegated?: string, reminder?: string): string;
40
+ /**
41
+ * Resolve the per-session reminder for the statusline, or '' when none is
42
+ * configured. A malformed reminders file is swallowed here on purpose — a broken
43
+ * prompt is worse than a missing reminder — while `agents reminders` surfaces it.
44
+ */
45
+ export declare function resolveReminderPart(sessionId?: string): string;
33
46
  export declare function runClaudeStatusLine(): Promise<number>;
34
47
  export declare function installClaudeStatusLine(versionHome: string): {
35
48
  changed: boolean;
@@ -5,6 +5,7 @@ import { spawnSync } from 'child_process';
5
5
  import { readClaudeHomeConfig } from './agent-spec/agents.js';
6
6
  import { atomicWriteFileSync } from './fs-atomic.js';
7
7
  import { mergeClaudeUsageCacheWindows } from './accounting/usage.js';
8
+ import { loadReminders, pickReminderForSession } from './reminders.js';
8
9
  export const CLAUDE_STATUSLINE_COMMAND = 'agents __claude-statusline';
9
10
  const DELEGATE_FILE = path.join('.agents', 'claude-statusline-delegate');
10
11
  // The private subcommand this feature runs. It is only ever invoked internally,
@@ -103,7 +104,16 @@ export function renderDelegate(payload, versionHome) {
103
104
  });
104
105
  return result.status === 0 ? result.stdout.trim() : '';
105
106
  }
106
- export function renderClaudeStatusLine(payload, host = os.hostname().split('.')[0] || os.hostname(), delegated = '') {
107
+ /**
108
+ * Format a reminder as a dimmed statusline part (empty string when none). The
109
+ * ◆ marker distinguishes it from the host/model/usage parts, and the ANSI dim
110
+ * keeps it quiet next to the live figures.
111
+ */
112
+ export function formatReminderPart(short) {
113
+ const text = short?.trim();
114
+ return text ? `\x1b[2m◆ ${text}\x1b[22m` : '';
115
+ }
116
+ export function renderClaudeStatusLine(payload, host = os.hostname().split('.')[0] || os.hostname(), delegated = '', reminder = '') {
107
117
  const model = payload.model?.display_name?.trim() || payload.model?.id?.trim() || 'model pending';
108
118
  const parts = [host, model];
109
119
  if (delegated)
@@ -114,8 +124,23 @@ export function renderClaudeStatusLine(payload, host = os.hostname().split('.')[
114
124
  parts.push(`5h ${Math.round(fiveHour)}%`);
115
125
  if (Number.isFinite(sevenDay))
116
126
  parts.push(`7d ${Math.round(sevenDay)}%`);
127
+ if (reminder)
128
+ parts.push(reminder);
117
129
  return parts.join(' · ');
118
130
  }
131
+ /**
132
+ * Resolve the per-session reminder for the statusline, or '' when none is
133
+ * configured. A malformed reminders file is swallowed here on purpose — a broken
134
+ * prompt is worse than a missing reminder — while `agents reminders` surfaces it.
135
+ */
136
+ export function resolveReminderPart(sessionId) {
137
+ try {
138
+ return formatReminderPart(pickReminderForSession(loadReminders(), sessionId)?.short);
139
+ }
140
+ catch {
141
+ return '';
142
+ }
143
+ }
119
144
  export async function runClaudeStatusLine() {
120
145
  const raw = await new Promise((resolve, reject) => {
121
146
  let input = '';
@@ -135,7 +160,7 @@ export async function runClaudeStatusLine() {
135
160
  const versionHome = versionHomeFromEnv(process.env);
136
161
  if (versionHome)
137
162
  ingestClaudeStatusLineUsage(payload, versionHome);
138
- process.stdout.write(renderClaudeStatusLine(payload, undefined, versionHome ? renderDelegate(raw, versionHome) : ''));
163
+ process.stdout.write(renderClaudeStatusLine(payload, undefined, versionHome ? renderDelegate(raw, versionHome) : '', resolveReminderPart(payload.session_id)));
139
164
  return 0;
140
165
  }
141
166
  export function installClaudeStatusLine(versionHome) {
@@ -36,7 +36,7 @@ import { ensureLockTarget } from '../fs-atomic.js';
36
36
  import { walkForFiles } from '../fs-walk.js';
37
37
  import { getBinaryPath, isVersionInstalled, resolveVersion, getVersionHomePath } from '../installations/versions.js';
38
38
  import { resolveClaudeSetupToken } from '../claude-account-token.js';
39
- import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from '../accounting/rotate.js';
39
+ import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, formatNoVerifiedUsageError, } from '../accounting/rotate.js';
40
40
  import { isHeadedDeviceRole, selfConfiguredDeviceRole } from '../device-config.js';
41
41
  import { isSelfUpdatingAgent, ROUTINE_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
42
42
  import { isCustomHarnessName, readProfile } from '../profiles.js';
@@ -959,12 +959,21 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
959
959
  let version;
960
960
  let rotation = null;
961
961
  let exhausted;
962
+ let noVerifiedUsage = false;
962
963
  try {
963
964
  const resolved = await (deps.resolveRunVersion ?? resolveRunVersion)(agent, strategy, cwd);
964
965
  version = resolved.version ?? undefined;
965
966
  rotation = resolved.rotation;
966
967
  exhausted = resolved.exhausted;
967
- if (rotation) {
968
+ noVerifiedUsage = resolved.noVerifiedUsage ?? false;
969
+ if (noVerifiedUsage) {
970
+ // Entirely stale usage (PHNX-2526): a routine is unattended, so there is
971
+ // no picker to divert to — it fails loud below rather than launch on a
972
+ // stale number. Do NOT log `rotation.picked` as a pick; it is the refused
973
+ // stale candidate, kept only for the failover chain.
974
+ process.stderr.write(`[agents] routine ${config.name}: ${strategy} found no ${agent} account with fresh usage — refusing to route on stale data\n`);
975
+ }
976
+ else if (rotation) {
968
977
  const label = rotation.picked.email
969
978
  ? `${rotation.picked.email} · ${agent}@${rotation.picked.version}`
970
979
  : `${agent}@${rotation.picked.version}`;
@@ -996,6 +1005,12 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
996
1005
  if (exhausted) {
997
1006
  throw new Error(formatNoHealthyAccountError(agent, strategy, exhausted));
998
1007
  }
1008
+ // Entirely stale usage is also NOT a "fall back to the default pin" case — the
1009
+ // default is exactly the account whose stale number can't be trusted. Fail the
1010
+ // run loud (NO_VERIFIED_USAGE) rather than hammer it every tick (PHNX-2526).
1011
+ if (noVerifiedUsage) {
1012
+ throw new Error(formatNoVerifiedUsageError(agent, strategy, rotation?.healthy ?? []));
1013
+ }
999
1014
  if (!version) {
1000
1015
  version = resolveVersion(agent, cwd) ?? undefined;
1001
1016
  }
@@ -6,7 +6,7 @@ import type { OwnerSinkStatus } from '../channels/owner-sink.js';
6
6
  import { type WindowsSshEnrollmentAudit } from './windows-ssh-enrollment.js';
7
7
  import type { SyncStatusRow, OrphanRow } from '../drift.js';
8
8
  import type { FetchStatusMarker } from '../auto-pull.js';
9
- import type { VersionResourceReport } from '../doctor-diff.js';
9
+ import { type VersionResourceReport } from '../doctor-diff.js';
10
10
  import type { FleetDivergence, FleetHookRuntimeState, FleetVersionSignIn } from './fleet-divergence.js';
11
11
  export type FindingSeverity = 'critical' | 'warning';
12
12
  /** A machine-stable class for a finding — drives {@link remediationFor} and lets
@@ -44,6 +44,24 @@ import { loginHint } from '../signin-badge.js';
44
44
  import { CONFIG_ENV_ISOLATED_AGENTS } from '../installations/shims.js';
45
45
  import { padToWidth, stringWidth } from '../text/width.js';
46
46
  import { windowsSshEnrollmentProblem } from './windows-ssh-enrollment.js';
47
+ import { DOCTOR_ALL_KINDS } from '../doctor-diff.js';
48
+ /**
49
+ * Singular noun per resource kind for finding messages. A naive `kind.replace(/s$/,'')`
50
+ * mangles `memory` → `memor` and `mcp` → `mcp` inconsistently, so the mapping is
51
+ * explicit. Keyed by `DoctorKind` so a new kind fails the type check until named.
52
+ */
53
+ const KIND_SINGULAR = {
54
+ commands: 'command',
55
+ skills: 'skill',
56
+ hooks: 'hook',
57
+ rules: 'rule',
58
+ mcp: 'mcp',
59
+ permissions: 'permission',
60
+ subagents: 'subagent',
61
+ plugins: 'plugin',
62
+ workflows: 'workflow',
63
+ memory: 'memory',
64
+ };
47
65
  const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
48
66
  /** Agents with NO per-version credential isolation: their login is shared across
49
67
  * every installed version, so a "log into THIS version" remediation would be a
@@ -87,8 +105,8 @@ export const ALL_FINDING_KINDS = [
87
105
  'hook-runtime-broken', // a wired hook's generated shim wrapper is missing/unusable
88
106
  'hook-runtime-visibility-unavailable', // remote CLI cannot report generated wrapper health
89
107
  'cli-missing', // a managed agent whose binary won't resolve
90
- 'missing-resource', // a missing command/skill/rule/mcp/permission/subagent
91
- 'content-drift', // a resource diverged from source
108
+ 'missing-resource', // a missing command/skill/rule/mcp/permission/subagent/workflow/memory
109
+ 'content-drift', // a resource (any synced kind) diverged from source
92
110
  'never-synced', // installed but never synced
93
111
  'stale', // sources changed since last sync
94
112
  'repo-behind', // a config repo behind origin
@@ -392,8 +410,8 @@ export function buildLocalFindings(input) {
392
410
  const missingPlugins = [];
393
411
  const missingOther = [];
394
412
  const drifted = [];
395
- for (const kind of ['commands', 'skills', 'hooks', 'rules', 'mcp', 'permissions', 'subagents', 'plugins', 'promptcuts']) {
396
- const singular = kind.replace(/s$/, '');
413
+ for (const kind of DOCTOR_ALL_KINDS) {
414
+ const singular = KIND_SINGULAR[kind];
397
415
  for (const r of report.kinds[kind] ?? []) {
398
416
  if (r.status === 'missing') {
399
417
  if (kind === 'hooks')
@@ -9,21 +9,37 @@
9
9
  * - missing — resolved source exists, not present in home
10
10
  * - extra — present in home, no source in any layer
11
11
  *
12
- * Coverage:
12
+ * Coverage — every kind `syncResourcesToVersion` writes is content-aware, so a
13
+ * byte change under an unchanged name is `diff`, never a false `ok` (PHNX-3504):
13
14
  * commands, skills, hooks, rules — full content compare with source layer.
14
- * mcp, permissions, subagents, plugins, promptcuts — presence-only.
15
+ * mcp — structural compare of the home server def vs resolved source.
16
+ * subagents — re-render source through the registry transform, byte-compare.
17
+ * workflows — layout-aware per-harness content compare (dir tree or file).
18
+ * memory — knowledge facts (~/.agents/memory/*.md), per-fact byte-compare.
19
+ * plugins — per-item content compare of the marketplace mirror.
20
+ * permissions — per-rule compare in the harness's native vocabulary for the
21
+ * representable harnesses (claude/opencode/cursor/droid/openclaw); the lossy
22
+ * TOML/flag harnesses stay presence-only with an honest `detail`, never a
23
+ * faked `ok`.
24
+ * A completeness test binds `DOCTOR_ALL_KINDS` to the writer set so a future
25
+ * synced kind cannot silently become a blind spot. `promptcuts` is NOT a kind —
26
+ * it is not version-scoped, so there is nothing per-home to diff.
15
27
  *
16
28
  * Intentional asymmetries (must mirror sync):
17
29
  * - hooks ignore the project layer (`syncResourcesToVersion` skips
18
30
  * project/.agents/hooks/ for safety).
19
- * - rules/AGENTS.md on agents without native @-import support is compared
20
- * against the compiled artifact, not the raw source file.
31
+ * - rules/AGENTS.md is compared against the preset composition the rules writer
32
+ * emits (`composeRulesFromState`), re-rendered from the current `subrules/`
33
+ * fragments — so a fragment edit is caught even though `rules/AGENTS.md`
34
+ * never changed. A non-@-import home compiled by `agents refresh-rules`
35
+ * carries a leading `COMPILED_HEADER`, which is stripped before comparing so
36
+ * a header-compiled home still reconciles (the header is not source content).
21
37
  */
22
38
  import type { AgentId } from './types.js';
23
39
  import type { DiscoveredPlugin } from './types.js';
24
40
  import { type HookWiringReport } from './hooks/install.js';
25
41
  import { type ResourceInventory } from './resource-inventory.js';
26
- export type DoctorKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'promptcuts';
42
+ export type DoctorKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'workflows' | 'memory';
27
43
  export type DiffStatus = 'ok' | 'diff' | 'missing' | 'extra';
28
44
  export type SourceLayer = 'project' | 'user' | 'system' | 'extra';
29
45
  export interface ResourceDiff {