@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.
Files changed (132) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +17 -14
  3. package/dist/bootstrap.js +3 -0
  4. package/dist/commands/accounts.js +23 -7
  5. package/dist/commands/exec.js +33 -23
  6. package/dist/commands/feed.js +20 -7
  7. package/dist/commands/import.js +2 -2
  8. package/dist/commands/models.js +2 -2
  9. package/dist/commands/monitors.js +3 -0
  10. package/dist/commands/permissions.js +2 -2
  11. package/dist/commands/projects.d.ts +26 -6
  12. package/dist/commands/projects.js +55 -22
  13. package/dist/commands/rules.js +1 -1
  14. package/dist/commands/send.js +35 -2
  15. package/dist/commands/sessions-inject.d.ts +58 -0
  16. package/dist/commands/sessions-inject.js +143 -7
  17. package/dist/commands/sessions-picker.js +1 -0
  18. package/dist/commands/share.js +43 -14
  19. package/dist/commands/ssh.js +205 -2
  20. package/dist/commands/traces.js +1 -1
  21. package/dist/lib/account-capabilities.js +0 -2
  22. package/dist/lib/account-registry.d.ts +3 -3
  23. package/dist/lib/account-registry.js +25 -7
  24. package/dist/lib/accounting/usage.d.ts +7 -2
  25. package/dist/lib/accounting/usage.js +142 -10
  26. package/dist/lib/acp/client.d.ts +1 -1
  27. package/dist/lib/acp/client.js +12 -1
  28. package/dist/lib/acp/harnesses.js +1 -1
  29. package/dist/lib/add-dir.js +0 -2
  30. package/dist/lib/agent-cli-commands.js +0 -2
  31. package/dist/lib/agent-spec/agents.d.ts +1 -1
  32. package/dist/lib/agent-spec/agents.js +2 -83
  33. package/dist/lib/boot-profile.d.ts +14 -0
  34. package/dist/lib/boot-profile.js +66 -0
  35. package/dist/lib/browser/ipc.js +9 -1
  36. package/dist/lib/browser/remote-control.d.ts +6 -3
  37. package/dist/lib/browser/remote-control.js +6 -3
  38. package/dist/lib/browser/service.d.ts +4 -1
  39. package/dist/lib/browser/service.js +7 -1
  40. package/dist/lib/channels/providers/desktop.d.ts +5 -4
  41. package/dist/lib/channels/providers/desktop.js +5 -4
  42. package/dist/lib/channels/registry.d.ts +6 -0
  43. package/dist/lib/channels/send.d.ts +11 -2
  44. package/dist/lib/channels/send.js +11 -2
  45. package/dist/lib/claude-account-token.js +108 -4
  46. package/dist/lib/cloud/rush.d.ts +10 -2
  47. package/dist/lib/cloud/rush.js +15 -9
  48. package/dist/lib/daemon/daemon.js +28 -8
  49. package/dist/lib/daemon/runner.js +6 -2
  50. package/dist/lib/devices/health.d.ts +38 -2
  51. package/dist/lib/devices/health.js +43 -5
  52. package/dist/lib/devices/worker-pick.d.ts +1 -1
  53. package/dist/lib/devices/worker-pick.js +4 -1
  54. package/dist/lib/exec.d.ts +2 -2
  55. package/dist/lib/exec.js +7 -34
  56. package/dist/lib/feed-broadcast.d.ts +47 -5
  57. package/dist/lib/feed-broadcast.js +127 -24
  58. package/dist/lib/hooks/install.js +0 -87
  59. package/dist/lib/installations/strategies.js +1 -1
  60. package/dist/lib/mcp-registry.js +0 -13
  61. package/dist/lib/mcp.js +2 -2
  62. package/dist/lib/model-tiers.js +1 -1
  63. package/dist/lib/models.js +0 -63
  64. package/dist/lib/monitors/engine.js +18 -0
  65. package/dist/lib/monitors/sources/command.js +13 -3
  66. package/dist/lib/monitors/sources/failure.d.ts +32 -0
  67. package/dist/lib/monitors/sources/failure.js +52 -0
  68. package/dist/lib/monitors/sources/types.d.ts +9 -0
  69. package/dist/lib/notify.d.ts +11 -0
  70. package/dist/lib/notify.js +17 -4
  71. package/dist/lib/owner-message.d.ts +55 -0
  72. package/dist/lib/owner-message.js +64 -0
  73. package/dist/lib/permissions-registry.d.ts +0 -2
  74. package/dist/lib/permissions-registry.js +3 -50
  75. package/dist/lib/permissions.d.ts +3 -17
  76. package/dist/lib/permissions.js +4 -73
  77. package/dist/lib/run-trace-sync.d.ts +15 -0
  78. package/dist/lib/run-trace-sync.js +43 -21
  79. package/dist/lib/rush-session.d.ts +19 -0
  80. package/dist/lib/rush-session.js +24 -0
  81. package/dist/lib/secrets/drivers/rush.js +2 -1
  82. package/dist/lib/secrets/filestore.d.ts +4 -0
  83. package/dist/lib/secrets/filestore.js +164 -3
  84. package/dist/lib/session/active.d.ts +10 -0
  85. package/dist/lib/session/active.js +3 -0
  86. package/dist/lib/session/cloud.js +2 -1
  87. package/dist/lib/session/db.d.ts +12 -1
  88. package/dist/lib/session/db.js +20 -1
  89. package/dist/lib/session/discover.js +81 -1
  90. package/dist/lib/session/linear.d.ts +13 -0
  91. package/dist/lib/session/linear.js +44 -0
  92. package/dist/lib/session/live-metadata.js +1 -0
  93. package/dist/lib/session/parse.js +2 -3
  94. package/dist/lib/session/prompt.d.ts +7 -1
  95. package/dist/lib/session/prompt.js +12 -2
  96. package/dist/lib/session/recovery.d.ts +21 -12
  97. package/dist/lib/session/recovery.js +29 -11
  98. package/dist/lib/session/remote/watch.js +5 -2
  99. package/dist/lib/session/state.js +11 -13
  100. package/dist/lib/share/backend.d.ts +2 -2
  101. package/dist/lib/share/backend.js +20 -9
  102. package/dist/lib/share/delete.d.ts +5 -1
  103. package/dist/lib/share/delete.js +7 -2
  104. package/dist/lib/share/http-error.d.ts +52 -0
  105. package/dist/lib/share/http-error.js +65 -0
  106. package/dist/lib/share/publish.d.ts +13 -3
  107. package/dist/lib/share/publish.js +19 -15
  108. package/dist/lib/share/worker-template.js +5 -1
  109. package/dist/lib/sink-format.d.ts +34 -0
  110. package/dist/lib/sink-format.js +17 -0
  111. package/dist/lib/smart-launch.js +27 -4
  112. package/dist/lib/staleness/detectors/permissions.js +0 -20
  113. package/dist/lib/staleness/writers/commands.js +1 -1
  114. package/dist/lib/staleness/writers/hooks.js +2 -2
  115. package/dist/lib/storage/index.d.ts +14 -0
  116. package/dist/lib/storage/index.js +14 -0
  117. package/dist/lib/storage/selection.d.ts +48 -0
  118. package/dist/lib/storage/selection.js +39 -0
  119. package/dist/lib/storage/visibility.d.ts +82 -0
  120. package/dist/lib/storage/visibility.js +99 -0
  121. package/dist/lib/subagents-registry.js +2 -12
  122. package/dist/lib/subagents.d.ts +0 -10
  123. package/dist/lib/subagents.js +0 -37
  124. package/dist/lib/teams/agents.js +3 -1
  125. package/dist/lib/teams/placement-probe.js +1 -0
  126. package/dist/lib/teams/scheduler.d.ts +8 -1
  127. package/dist/lib/teams/scheduler.js +4 -1
  128. package/dist/lib/traces/backend.js +13 -2
  129. package/dist/lib/types.d.ts +1 -1
  130. package/dist/lib/worktree/held.d.ts +166 -0
  131. package/dist/lib/worktree/held.js +368 -0
  132. package/package.json +2 -2
@@ -90,15 +90,29 @@ export function formatEmptyAutoPoolError() {
90
90
  'mark one with `agents devices role <name> worker`, or widen the pool with `agents config set auto.pool all`.');
91
91
  }
92
92
  export function formatNoHealthyDeviceError(pool, signals, agent) {
93
+ let timedOutCount = 0;
93
94
  const excluded = pool.map((key) => {
94
95
  const signal = signals.get(key);
95
- const reason = signal?.reachable !== true
96
- ? 'unreachable'
97
- : signal.headroom === 'loaded'
96
+ let reason;
97
+ if (signal?.reachable === true) {
98
+ reason = signal.headroom === 'loaded'
98
99
  ? 'overloaded'
99
100
  : signal.installed !== true || signal.signedIn !== true
100
101
  ? 'no ready harness account'
101
102
  : 'ineligible';
103
+ }
104
+ else if (signal?.timedOut) {
105
+ // A slow link is not an offline box. Saying "unreachable" here sent
106
+ // operators hunting a fleet outage that did not exist (PHNX-3682).
107
+ timedOutCount++;
108
+ reason = 'probe timed out';
109
+ }
110
+ else if (signal === undefined) {
111
+ reason = 'no probe signal';
112
+ }
113
+ else {
114
+ reason = 'unreachable';
115
+ }
102
116
  return `${key} (${reason})`;
103
117
  }).join(', ');
104
118
  const target = agent ? `can run ${agent}` : "for 'run auto'";
@@ -108,7 +122,16 @@ export function formatNoHealthyDeviceError(pool, signals, agent) {
108
122
  // in this error's own candidate set, not just the ones with a doc.
109
123
  const marked = describeAutoPool({ roster: pool });
110
124
  const poolNote = marked ? ` [pool: ${marked}]` : '';
111
- return `agents: no healthy device ${target}${poolNote} — excluded: ${excluded}; earliest window resets unknown`;
125
+ // Only talk about usage windows when a device was actually turned away for
126
+ // one. A pool that timed out needs a link/latency hint, not a reset time.
127
+ const scope = timedOutCount === pool.length
128
+ ? `every probe (${pool.length}) exceeded`
129
+ : `${timedOutCount} of ${pool.length} probes exceeded`;
130
+ const hint = timedOutCount > 0
131
+ ? `; ${scope} the probe budget — those devices are likely up but slow to answer`
132
+ + ' (relayed Tailscale paths). Retry, or check `tailscale status` for a direct path.'
133
+ : '; earliest window resets unknown';
134
+ return `agents: no healthy device ${target}${poolNote} — excluded: ${excluded}${hint}`;
112
135
  }
113
136
  /**
114
137
  * Pick the least-loaded healthy device that can run `agent` when the harness is
@@ -202,25 +202,6 @@ function buildDroidDetector() {
202
202
  },
203
203
  };
204
204
  }
205
- function buildKiroDetector() {
206
- return {
207
- kind: 'permissions',
208
- agent: 'kiro',
209
- list({ versionHome }) {
210
- const permissionsPath = path.join(versionHome, '.kiro', 'settings', 'permissions.yaml');
211
- if (!fs.existsSync(permissionsPath))
212
- return [];
213
- try {
214
- const config = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
215
- if (config && Array.isArray(config.rules) && config.rules.length > 0) {
216
- return discoverPermissionGroups().map(g => g.name);
217
- }
218
- }
219
- catch { /* parse fail */ }
220
- return [];
221
- },
222
- };
223
- }
224
205
  function buildOpenClawDetector() {
225
206
  return {
226
207
  kind: 'permissions',
@@ -296,7 +277,6 @@ const handlers = {
296
277
  kimi: buildKimiDetector,
297
278
  cursor: buildCursorDetector,
298
279
  droid: buildDroidDetector,
299
- kiro: buildKiroDetector,
300
280
  openclaw: buildOpenClawDetector,
301
281
  copilot: buildCopilotDetector,
302
282
  hermes: buildHermesDetector,
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * - native command file — `{agentDir}/<commandsSubdir>/<name>.md` (or .toml
15
15
  * when the agent's format is toml). Standard path for Claude, Codex
16
- * < 0.117.0, Cursor, OpenCode, Copilot, Amp, Kiro, Roo, Antigravity.
16
+ * < 0.117.0, Cursor, OpenCode, Copilot, Amp, Roo, Antigravity.
17
17
  *
18
18
  * Source resolution is `resolveCommandSource` (user → system → extras —
19
19
  * project layer intentionally excluded).
@@ -90,9 +90,9 @@ function buildHooksWriter(agent) {
90
90
  // so subrule-bundled guards (absolute paths outside the central hooks/
91
91
  // copy set) get registered into ~/.grok/hooks/hooks.json via
92
92
  // registerHooksForGrok — file copy alone only sees top-level available.hooks
93
- // names (RUSH-1353). Copilot/Kiro/Goose load managed *.json under their
93
+ // names (RUSH-1353). Copilot/Goose load managed *.json under their
94
94
  // hooks dirs the same way.
95
- if (agent === 'claude' || agent === 'codex' || agent === 'antigravity' || agent === 'kimi' || agent === 'droid' || agent === 'copilot' || agent === 'kiro' || agent === 'goose' || agent === 'cursor' || agent === 'grok' || agent === 'hermes' || agent === 'muse') {
95
+ if (agent === 'claude' || agent === 'codex' || agent === 'antigravity' || agent === 'kimi' || agent === 'droid' || agent === 'copilot' || agent === 'goose' || agent === 'cursor' || agent === 'grok' || agent === 'hermes' || agent === 'muse') {
96
96
  registerHooksToSettings(agent, versionHome);
97
97
  }
98
98
  return { synced, paths };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `lib/storage` — the surface-agnostic core the managed-storage surfaces share:
3
+ *
4
+ * - {@link selection} the ONE managed-vs-BYO selection policy.
5
+ * - {@link visibility} the ONE visibility model + product default (`me`).
6
+ *
7
+ * Consume the SELECTION + VISIBILITY POLICY from here; keep each surface's typed,
8
+ * `kind`-tagged backend adapter (endpoint, namespace, covers) in that surface's
9
+ * own module. `agents artifacts share` (`lib/share/`) and `agents traces`
10
+ * (`lib/traces/`) are the two current adapters; the `sessions` sync adapter is
11
+ * the next consumer.
12
+ */
13
+ export { type StorageBackendKind, type StorageSelectionOpts, selectStorageBackendKind, isManagedSelection, } from './selection.js';
14
+ export { type ShareVisibility, type VisibilityFlags, PUBLISH_VISIBILITY_LEVELS, EDITABLE_VISIBILITY_LEVELS, MANAGED_DEFAULT_VISIBILITY, BYO_DEFAULT_VISIBILITY, defaultVisibilityForBackend, explicitVisibility, resolveVisibility, publishVisibility, } from './visibility.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `lib/storage` — the surface-agnostic core the managed-storage surfaces share:
3
+ *
4
+ * - {@link selection} the ONE managed-vs-BYO selection policy.
5
+ * - {@link visibility} the ONE visibility model + product default (`me`).
6
+ *
7
+ * Consume the SELECTION + VISIBILITY POLICY from here; keep each surface's typed,
8
+ * `kind`-tagged backend adapter (endpoint, namespace, covers) in that surface's
9
+ * own module. `agents artifacts share` (`lib/share/`) and `agents traces`
10
+ * (`lib/traces/`) are the two current adapters; the `sessions` sync adapter is
11
+ * the next consumer.
12
+ */
13
+ export { selectStorageBackendKind, isManagedSelection, } from './selection.js';
14
+ export { PUBLISH_VISIBILITY_LEVELS, EDITABLE_VISIBILITY_LEVELS, MANAGED_DEFAULT_VISIBILITY, BYO_DEFAULT_VISIBILITY, defaultVisibilityForBackend, explicitVisibility, resolveVisibility, publishVisibility, } from './visibility.js';
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The ONE managed-vs-BYO storage-backend selection policy.
3
+ *
4
+ * Every surface that persists to Phoenix-managed storage (`agents artifacts
5
+ * share` today, `agents sessions` sync next) makes the SAME choice: run on OUR
6
+ * managed infrastructure when the caller is signed in to Phoenix, and only fall
7
+ * back to a bring-your-own bucket when the caller explicitly asked for it. This
8
+ * module owns that decision so it is not re-derived — and re-drifted — per
9
+ * surface.
10
+ *
11
+ * What lives here is ONLY the identity + selection policy. It deliberately does
12
+ * NOT know a surface's endpoint, namespace shape, public-read semantics, or OG
13
+ * covers — those differ (share uses an email-handle namespace with public reads
14
+ * and covers; a traces/sessions adapter uses the userId and differs again). Each
15
+ * surface keeps its own DISCRIMINATED, typed adapter that reads this decision and
16
+ * returns its own `kind`-tagged backend. See `lib/share/backend.ts` and
17
+ * `lib/traces/backend.ts` for the two adapters.
18
+ */
19
+ import { type PhoenixSession } from '../identity/client.js';
20
+ /** The two legitimate principals any managed-capable surface can resolve to. */
21
+ export type StorageBackendKind = 'managed' | 'byo';
22
+ export interface StorageSelectionOpts {
23
+ /**
24
+ * True when the SURFACE detected an explicit bring-your-own override — a
25
+ * `--byo` flag, a caller-supplied static write token, a `…_BACKEND=byo` env,
26
+ * or a full BYO endpoint config. Detecting WHICH signals count is the
27
+ * surface's job (they differ per surface); this policy only honors the boolean.
28
+ */
29
+ byoOverride?: boolean;
30
+ /**
31
+ * DI seam for the Phoenix session. `undefined` reads the real persisted
32
+ * session (`readSession()`); `null` means "explicitly signed out".
33
+ */
34
+ session?: PhoenixSession | null;
35
+ }
36
+ /**
37
+ * Pick the storage principal. Managed when signed in (`readSession() != null`)
38
+ * AND the surface reported no explicit BYO override; otherwise BYO.
39
+ *
40
+ * This is not a fallback chain — it is a single decision. A surface that cannot
41
+ * authenticate EITHER principal (signed out AND no BYO config) still reads `byo`
42
+ * here and fails loud in its own adapter, where the actionable "run auth login
43
+ * or set up your bucket" message belongs.
44
+ */
45
+ export declare function selectStorageBackendKind(opts?: StorageSelectionOpts): StorageBackendKind;
46
+ /** True when the shared policy resolves to the managed principal. Thin sugar
47
+ * over {@link selectStorageBackendKind} for the common boolean check. */
48
+ export declare function isManagedSelection(opts?: StorageSelectionOpts): boolean;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The ONE managed-vs-BYO storage-backend selection policy.
3
+ *
4
+ * Every surface that persists to Phoenix-managed storage (`agents artifacts
5
+ * share` today, `agents sessions` sync next) makes the SAME choice: run on OUR
6
+ * managed infrastructure when the caller is signed in to Phoenix, and only fall
7
+ * back to a bring-your-own bucket when the caller explicitly asked for it. This
8
+ * module owns that decision so it is not re-derived — and re-drifted — per
9
+ * surface.
10
+ *
11
+ * What lives here is ONLY the identity + selection policy. It deliberately does
12
+ * NOT know a surface's endpoint, namespace shape, public-read semantics, or OG
13
+ * covers — those differ (share uses an email-handle namespace with public reads
14
+ * and covers; a traces/sessions adapter uses the userId and differs again). Each
15
+ * surface keeps its own DISCRIMINATED, typed adapter that reads this decision and
16
+ * returns its own `kind`-tagged backend. See `lib/share/backend.ts` and
17
+ * `lib/traces/backend.ts` for the two adapters.
18
+ */
19
+ import { readSession } from '../identity/client.js';
20
+ /**
21
+ * Pick the storage principal. Managed when signed in (`readSession() != null`)
22
+ * AND the surface reported no explicit BYO override; otherwise BYO.
23
+ *
24
+ * This is not a fallback chain — it is a single decision. A surface that cannot
25
+ * authenticate EITHER principal (signed out AND no BYO config) still reads `byo`
26
+ * here and fails loud in its own adapter, where the actionable "run auth login
27
+ * or set up your bucket" message belongs.
28
+ */
29
+ export function selectStorageBackendKind(opts = {}) {
30
+ if (opts.byoOverride === true)
31
+ return 'byo';
32
+ const session = opts.session === undefined ? readSession() : opts.session;
33
+ return session != null ? 'managed' : 'byo';
34
+ }
35
+ /** True when the shared policy resolves to the managed principal. Thin sugar
36
+ * over {@link selectStorageBackendKind} for the common boolean check. */
37
+ export function isManagedSelection(opts = {}) {
38
+ return selectStorageBackendKind(opts) === 'managed';
39
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The ONE visibility model, identical on every surface that publishes to managed
3
+ * storage. Three levels the operator ever chooses between:
4
+ *
5
+ * - PRIVATE = `me` — owner-only, Phoenix-gated. THE DEFAULT for a new
6
+ * managed share (see {@link MANAGED_DEFAULT_VISIBILITY}).
7
+ * - TEAM = `org` — everyone at the sharer's signed-in email DOMAIN,
8
+ * Phoenix-gated. Automatic from the domain — there is
9
+ * no organization to create and no member to add.
10
+ * - PUBLIC = `public` — explicit opt-in; listed in the gallery, gets an OG
11
+ * card.
12
+ *
13
+ * Two capability-URL levels remain for the power path (unguessable link, not a
14
+ * login gate): `unlisted` (obscurity — noindex, gallery-hidden, still
15
+ * world-readable) and `private`-token-gated (`--protected`: a `?k=` key the
16
+ * Worker checks, 404 without it). `me`/`org` require a Phoenix session; the
17
+ * Worker refuses them for a bare WRITE_TOKEN (BYO) publish, which is why the BYO
18
+ * default stays `public`.
19
+ *
20
+ * The names match the Worker's own metadata vocabulary (`lib/share/worker-template.ts`),
21
+ * so this module is the single client-side source of truth for the level set and
22
+ * the DEFAULT, reusable by any surface.
23
+ */
24
+ /** Every visibility level the Worker understands. `me`/`org` are the
25
+ * Phoenix-gated (login-required) levels; `public`/`unlisted`/`private` are the
26
+ * link-reachable levels. */
27
+ export type ShareVisibility = 'public' | 'unlisted' | 'private' | 'me' | 'org';
28
+ /** The levels a publish (`share <file> --visibility`) may select. */
29
+ export declare const PUBLISH_VISIBILITY_LEVELS: readonly ShareVisibility[];
30
+ /** The levels an ALREADY-published page may be re-scoped to in place
31
+ * (`share visibility <target> <level>`). Excludes `private`: re-scoping to
32
+ * token-gated needs a fresh viewer token, which only the publish path mints. */
33
+ export declare const EDITABLE_VISIBILITY_LEVELS: readonly ShareVisibility[];
34
+ /**
35
+ * The product default for a MANAGED (signed-in) share: PRIVATE = owner-only,
36
+ * Phoenix-gated. A publish with no visibility flag lands here.
37
+ */
38
+ export declare const MANAGED_DEFAULT_VISIBILITY: ShareVisibility;
39
+ /**
40
+ * The default for a BYO (bring-your-own bucket) publish. `me`/`org` are refused
41
+ * server-side for a WRITE_TOKEN publish — there is no Phoenix owner to gate on —
42
+ * so a BYO share with no flag stays `public`, matching the pre-managed behavior.
43
+ */
44
+ export declare const BYO_DEFAULT_VISIBILITY: ShareVisibility;
45
+ import type { StorageBackendKind } from './selection.js';
46
+ /** The no-flags default for a given storage backend: `me` on managed,
47
+ * `public` on BYO. */
48
+ export declare function defaultVisibilityForBackend(kind: StorageBackendKind): ShareVisibility;
49
+ export interface VisibilityFlags {
50
+ /** An explicit `--visibility <level>`. */
51
+ visibility?: ShareVisibility;
52
+ /** `--unlisted` / `--private` (obscurity, not read-auth). */
53
+ unlisted?: boolean;
54
+ /** `--protected` — token-gated read auth (maps to `private`). */
55
+ protected?: boolean;
56
+ }
57
+ /**
58
+ * The visibility the caller EXPLICITLY asked for, or `undefined` when they
59
+ * passed no visibility signal at all. `--protected` wins over `--unlisted`
60
+ * (stronger control), which wins over an explicit `--visibility`.
61
+ *
62
+ * The distinction between "explicit" and "no preference" is what lets a caller
63
+ * apply the product default only when nothing was asked — a caller that passes
64
+ * `--visibility public` gets `public`, never the `me` default.
65
+ */
66
+ export declare function explicitVisibility(opts?: VisibilityFlags): ShareVisibility | undefined;
67
+ /**
68
+ * Resolve visibility from the caller's flags, falling back to `fallback` when no
69
+ * explicit signal was given. The library fallback stays `public` so a caller
70
+ * that hasn't opted into the managed private default (e.g. an existing lib
71
+ * consumer) is never silently flipped; the surface applies the product default
72
+ * itself via {@link defaultVisibilityForBackend}.
73
+ */
74
+ export declare function resolveVisibility(opts?: VisibilityFlags, fallback?: ShareVisibility): ShareVisibility;
75
+ /**
76
+ * The visibility a PUBLISH should stamp: the caller's explicit flag if any, else
77
+ * the product default for the resolved backend (`me` on managed, `public` on
78
+ * BYO). This is the one call a publishing surface makes — `agents artifacts
79
+ * share` today, `agents sessions` sync next — so "private by default when signed
80
+ * in" is decided in exactly one place.
81
+ */
82
+ export declare function publishVisibility(opts: VisibilityFlags, kind: StorageBackendKind): ShareVisibility;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The ONE visibility model, identical on every surface that publishes to managed
3
+ * storage. Three levels the operator ever chooses between:
4
+ *
5
+ * - PRIVATE = `me` — owner-only, Phoenix-gated. THE DEFAULT for a new
6
+ * managed share (see {@link MANAGED_DEFAULT_VISIBILITY}).
7
+ * - TEAM = `org` — everyone at the sharer's signed-in email DOMAIN,
8
+ * Phoenix-gated. Automatic from the domain — there is
9
+ * no organization to create and no member to add.
10
+ * - PUBLIC = `public` — explicit opt-in; listed in the gallery, gets an OG
11
+ * card.
12
+ *
13
+ * Two capability-URL levels remain for the power path (unguessable link, not a
14
+ * login gate): `unlisted` (obscurity — noindex, gallery-hidden, still
15
+ * world-readable) and `private`-token-gated (`--protected`: a `?k=` key the
16
+ * Worker checks, 404 without it). `me`/`org` require a Phoenix session; the
17
+ * Worker refuses them for a bare WRITE_TOKEN (BYO) publish, which is why the BYO
18
+ * default stays `public`.
19
+ *
20
+ * The names match the Worker's own metadata vocabulary (`lib/share/worker-template.ts`),
21
+ * so this module is the single client-side source of truth for the level set and
22
+ * the DEFAULT, reusable by any surface.
23
+ */
24
+ /** The levels a publish (`share <file> --visibility`) may select. */
25
+ export const PUBLISH_VISIBILITY_LEVELS = [
26
+ 'public',
27
+ 'unlisted',
28
+ 'private',
29
+ 'me',
30
+ 'org',
31
+ ];
32
+ /** The levels an ALREADY-published page may be re-scoped to in place
33
+ * (`share visibility <target> <level>`). Excludes `private`: re-scoping to
34
+ * token-gated needs a fresh viewer token, which only the publish path mints. */
35
+ export const EDITABLE_VISIBILITY_LEVELS = [
36
+ 'public',
37
+ 'unlisted',
38
+ 'me',
39
+ 'org',
40
+ ];
41
+ /**
42
+ * The product default for a MANAGED (signed-in) share: PRIVATE = owner-only,
43
+ * Phoenix-gated. A publish with no visibility flag lands here.
44
+ */
45
+ export const MANAGED_DEFAULT_VISIBILITY = 'me';
46
+ /**
47
+ * The default for a BYO (bring-your-own bucket) publish. `me`/`org` are refused
48
+ * server-side for a WRITE_TOKEN publish — there is no Phoenix owner to gate on —
49
+ * so a BYO share with no flag stays `public`, matching the pre-managed behavior.
50
+ */
51
+ export const BYO_DEFAULT_VISIBILITY = 'public';
52
+ /** The no-flags default for a given storage backend: `me` on managed,
53
+ * `public` on BYO. */
54
+ export function defaultVisibilityForBackend(kind) {
55
+ return kind === 'managed' ? MANAGED_DEFAULT_VISIBILITY : BYO_DEFAULT_VISIBILITY;
56
+ }
57
+ /**
58
+ * The visibility the caller EXPLICITLY asked for, or `undefined` when they
59
+ * passed no visibility signal at all. `--protected` wins over `--unlisted`
60
+ * (stronger control), which wins over an explicit `--visibility`.
61
+ *
62
+ * The distinction between "explicit" and "no preference" is what lets a caller
63
+ * apply the product default only when nothing was asked — a caller that passes
64
+ * `--visibility public` gets `public`, never the `me` default.
65
+ */
66
+ export function explicitVisibility(opts = {}) {
67
+ if (opts.protected === true)
68
+ return 'private';
69
+ if (opts.unlisted === true)
70
+ return 'unlisted';
71
+ if (opts.visibility === 'public' ||
72
+ opts.visibility === 'unlisted' ||
73
+ opts.visibility === 'private' ||
74
+ opts.visibility === 'me' ||
75
+ opts.visibility === 'org') {
76
+ return opts.visibility;
77
+ }
78
+ return undefined;
79
+ }
80
+ /**
81
+ * Resolve visibility from the caller's flags, falling back to `fallback` when no
82
+ * explicit signal was given. The library fallback stays `public` so a caller
83
+ * that hasn't opted into the managed private default (e.g. an existing lib
84
+ * consumer) is never silently flipped; the surface applies the product default
85
+ * itself via {@link defaultVisibilityForBackend}.
86
+ */
87
+ export function resolveVisibility(opts = {}, fallback = 'public') {
88
+ return explicitVisibility(opts) ?? fallback;
89
+ }
90
+ /**
91
+ * The visibility a PUBLISH should stamp: the caller's explicit flag if any, else
92
+ * the product default for the resolved backend (`me` on managed, `public` on
93
+ * BYO). This is the one call a publishing surface makes — `agents artifacts
94
+ * share` today, `agents sessions` sync next — so "private by default when signed
95
+ * in" is decided in exactly one place.
96
+ */
97
+ export function publishVisibility(opts, kind) {
98
+ return explicitVisibility(opts) ?? defaultVisibilityForBackend(kind);
99
+ }
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * - `flatFile` one `<name><ext>` file, body from a `transform` fn.
17
17
  * (claude, grok, pi, droid, codex, opencode, copilot,
18
- * cursor, kiro, goose, kimi)
18
+ * cursor, goose, kimi)
19
19
  * - `dirFile` a `<name>/` directory holding one generated `<file>`.
20
20
  * (antigravity: `<name>/agent.md`)
21
21
  * - `dirCopy` copy the whole source directory to `<name>/`, applying
@@ -35,7 +35,7 @@ import * as TOML from 'smol-toml';
35
35
  import * as yaml from 'yaml';
36
36
  import { safeJoin } from './paths.js';
37
37
  import { filesContentMatch, normalizeResourceContent } from './resource-content-diff.js';
38
- import { parseSubagentFrontmatter, transformSubagentForClaude, transformSubagentForCodex, transformSubagentForCopilot, transformSubagentForCursor, transformSubagentForDroid, transformSubagentForGoose, transformSubagentForKiro, transformSubagentForOpenCode, transformSubagentForAntigravity, } from './subagents.js';
38
+ import { parseSubagentFrontmatter, transformSubagentForClaude, transformSubagentForCodex, transformSubagentForCopilot, transformSubagentForCursor, transformSubagentForDroid, transformSubagentForGoose, transformSubagentForOpenCode, transformSubagentForAntigravity, } from './subagents.js';
39
39
  /** Read a file's UTF-8 content, or null when it is missing/unreadable. */
40
40
  function readFileSafe(filePath) {
41
41
  try {
@@ -54,7 +54,6 @@ function metaFrontmatterSkip(filePath) {
54
54
  function metaFrontmatterFallback(filePath, name) {
55
55
  return parseSubagentFrontmatter(filePath) ?? { name, description: '' };
56
56
  }
57
- /** Kiro custom-agent JSON: read name/description/model; skip on parse error. */
58
57
  function metaJson(filePath, name) {
59
58
  try {
60
59
  const cfg = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
@@ -283,9 +282,6 @@ export const SUBAGENT_TARGETS = {
283
282
  // Tier 1 -- flat markdown, Claude-compatible flatten.
284
283
  claude: flatFile({ subdir: ['.claude', 'agents'], ext: '.md', transform: transformSubagentForClaude }),
285
284
  grok: flatFile({ subdir: ['.grok', 'agents'], ext: '.md', transform: transformSubagentForClaude }),
286
- // Oh My Pi discovers Claude-shaped agent markdown from ~/.omp/agent/agents/
287
- // (`omp agents unpack` writes there); frontmatter name/description + body.
288
- pi: flatFile({ subdir: ['.omp', 'agent', 'agents'], ext: '.md', transform: transformSubagentForClaude, readMeta: metaFrontmatterFallback }),
289
285
  droid: flatFile({ subdir: ['.factory', 'droids'], ext: '.md', transform: transformSubagentForDroid }),
290
286
  // Bespoke frontmatter/format, still one flat file.
291
287
  codex: flatFile({
@@ -312,12 +308,6 @@ export const SUBAGENT_TARGETS = {
312
308
  transform: transformSubagentForCursor,
313
309
  readMeta: metaFrontmatterFallback,
314
310
  }),
315
- kiro: flatFile({
316
- subdir: ['.kiro', 'agents'],
317
- ext: '.json',
318
- transform: transformSubagentForKiro,
319
- readMeta: metaJson,
320
- }),
321
311
  goose: flatFile({
322
312
  subdir: ['.config', 'goose', 'agents'],
323
313
  ext: '.yaml',
@@ -106,16 +106,6 @@ export declare function transformSubagentForOpenCode(subagentDir: string): strin
106
106
  * https://developers.openai.com/codex/subagents (custom agents section)
107
107
  */
108
108
  export declare function transformSubagentForCodex(subagentDir: string): string;
109
- /**
110
- * Transform a subagent into a Kiro CLI custom-agent JSON file.
111
- *
112
- * Kiro custom agents live in `~/.kiro/agents/<name>.json` (or `.kiro/agents/`
113
- * workspace-local) and declare name, description, prompt, tools, and optional
114
- * model. We flatten the AGENT.md frontmatter + body plus any sibling .md files
115
- * as sections into a single `prompt`, and expose the standard built-in tool
116
- * set so the subagent can actually run.
117
- */
118
- export declare function transformSubagentForKiro(subagentDir: string): string;
119
109
  /**
120
110
  * Transform a subagent into a Goose recipe YAML file.
121
111
  *
@@ -403,43 +403,6 @@ function flattenSubagentInstructions(subagentDir) {
403
403
  }
404
404
  return instructions;
405
405
  }
406
- /**
407
- * Transform a subagent into a Kiro CLI custom-agent JSON file.
408
- *
409
- * Kiro custom agents live in `~/.kiro/agents/<name>.json` (or `.kiro/agents/`
410
- * workspace-local) and declare name, description, prompt, tools, and optional
411
- * model. We flatten the AGENT.md frontmatter + body plus any sibling .md files
412
- * as sections into a single `prompt`, and expose the standard built-in tool
413
- * set so the subagent can actually run.
414
- */
415
- export function transformSubagentForKiro(subagentDir) {
416
- const agentMd = path.join(subagentDir, 'AGENT.md');
417
- const frontmatter = parseSubagentFrontmatter(agentMd);
418
- const body = getSubagentBody(agentMd);
419
- if (!frontmatter) {
420
- throw new Error(`Invalid AGENT.md in ${subagentDir}`);
421
- }
422
- const files = fs.readdirSync(subagentDir)
423
- .filter(f => f.endsWith('.md') && f !== 'AGENT.md')
424
- .sort();
425
- let prompt = body;
426
- for (const file of files) {
427
- const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
428
- const sectionName = file.replace('.md', '');
429
- const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
430
- prompt += `\n\n## ${title}\n\n${content}`;
431
- }
432
- const config = {
433
- name: frontmatter.name,
434
- description: frontmatter.description,
435
- prompt,
436
- tools: ['read', 'write', 'shell', 'web_search', 'web_fetch'],
437
- };
438
- if (frontmatter.model) {
439
- config.model = frontmatter.model;
440
- }
441
- return JSON.stringify(config, null, 2);
442
- }
443
406
  /**
444
407
  * Transform a subagent into a Goose recipe YAML file.
445
408
  *
@@ -2361,7 +2361,9 @@ export class AgentManager {
2361
2361
  ? `at its agents.max-concurrent cap (${e.detail} running)`
2362
2362
  : e.reason === 'not-installed'
2363
2363
  ? `does not have ${this.placementAgentLabel(agent)} installed`
2364
- : e.reason;
2364
+ : e.reason === 'probe-timed-out'
2365
+ ? 'did not answer the probe in time (likely up but on a slow/relayed link)'
2366
+ : e.reason;
2365
2367
  console.error(chalk.dim(`[placement] '${e.device}' excluded from auto-pick — ${why}`));
2366
2368
  }
2367
2369
  }
@@ -137,6 +137,7 @@ export async function probePoolSignals(pool, agent, opts = {}) {
137
137
  continue; // fully unknown device — leave it out of the map
138
138
  signals.set(name, {
139
139
  reachable: s?.reachable,
140
+ timedOut: s?.timedOut,
140
141
  headroom: s ? headroom(s) : undefined,
141
142
  loadPercent: s?.loadPercent,
142
143
  memPercent: s?.memPercent,
@@ -10,6 +10,13 @@ import type { Headroom } from '../devices/health.js';
10
10
  export interface DevicePlacementSignal {
11
11
  /** SSH probe answered. `false` → excluded (unreachable). undefined → unknown. */
12
12
  reachable?: boolean;
13
+ /**
14
+ * The probe was killed for exceeding its budget rather than failing to
15
+ * connect. Ranking treats it exactly like `reachable:false` — an
16
+ * unresponsive box is still not placeable — but the operator-facing
17
+ * exclusion reason says "probe timed out", not "unreachable" (PHNX-3682).
18
+ */
19
+ timedOut?: boolean;
13
20
  /** Headroom bucket from load+memory. `'loaded'` → excluded (overloaded). */
14
21
  headroom?: Headroom;
15
22
  /** Normalized CPU load percent (finer rank tiebreak within a headroom tier). */
@@ -60,7 +67,7 @@ export interface PlacementOptions {
60
67
  preferred?: ReadonlySet<string>;
61
68
  }
62
69
  /** Why a device was excluded from the viable set, for the fail-loud message. */
63
- export type ExclusionReason = 'unreachable' | 'overloaded' | 'capped' | 'not-installed';
70
+ export type ExclusionReason = 'unreachable' | 'probe-timed-out' | 'overloaded' | 'capped' | 'not-installed';
64
71
  /** A pool device dropped from the auto-pick, with the reason + live detail. */
65
72
  export interface ExcludedDevice {
66
73
  device: string;
@@ -199,7 +199,10 @@ export function classifyExclusions(devices, roster, opts) {
199
199
  for (const d of devices) {
200
200
  const s = signals?.get(d);
201
201
  if (s?.reachable === false) {
202
- excluded.push({ device: d, reason: 'unreachable' });
202
+ // A probe killed for exceeding its budget is a slow link, not a box that
203
+ // is down. Same exclusion either way — an unresponsive device is still not
204
+ // placeable — but the operator sees which one it was (PHNX-3682).
205
+ excluded.push({ device: d, reason: s.timedOut ? 'probe-timed-out' : 'unreachable' });
203
206
  continue;
204
207
  }
205
208
  if (s?.installed === false) {
@@ -10,6 +10,7 @@
10
10
  * `agents artifacts setup`.
11
11
  */
12
12
  import { readSession } from '../identity/client.js';
13
+ import { selectStorageBackendKind } from '../storage/selection.js';
13
14
  export const DEFAULT_TRACES_DOMAIN = 'traces.agents-cli.sh';
14
15
  /**
15
16
  * Resolve the backend for the current machine. Throws when not signed in.
@@ -21,11 +22,21 @@ export const DEFAULT_TRACES_DOMAIN = 'traces.agents-cli.sh';
21
22
  export function resolveTracesBackend() {
22
23
  const envBase = (process.env['AGENTS_TRACES_BASE_URL'] ?? '').replace(/\/+$/, '').trim();
23
24
  const envToken = (process.env['AGENTS_TRACES_WRITE_TOKEN'] ?? '').trim();
24
- if (envBase && envToken) {
25
- return { baseUrl: envBase, token: envToken, userId: 'byo' };
25
+ // The traces surface's only BYO signal is the full BASE_URL + WRITE_TOKEN env
26
+ // pair; the managed-vs-BYO decision itself is the shared selection policy.
27
+ const byoOverride = Boolean(envBase && envToken);
28
+ if (selectStorageBackendKind({ byoOverride }) === 'byo') {
29
+ if (byoOverride) {
30
+ return { baseUrl: envBase, token: envToken, userId: 'byo' };
31
+ }
32
+ // Not signed in and no BYO env pair — the managed principal is the only one
33
+ // this surface exposes, so fail loud with the login hint.
34
+ throw new Error("Not signed in. Run 'agents auth login' to sync traces to your Phoenix account.");
26
35
  }
27
36
  const session = readSession();
28
37
  if (!session) {
38
+ // selectStorageBackendKind read the same session and returned 'managed', so a
39
+ // null here means it was cleared between the two reads — treat as signed out.
29
40
  throw new Error("Not signed in. Run 'agents auth login' to sync traces to your Phoenix account.");
30
41
  }
31
42
  if (!session.access_token) {
@@ -8,7 +8,7 @@
8
8
  import type { CloudProviderId } from './cloud/types.js';
9
9
  import type { FeedBroadcastConfig } from './feed-broadcast.js';
10
10
  /** Unique identifier for a current or legacy AI coding agent. */
11
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi' | 'muse' | 'warp';
11
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'muse' | 'warp';
12
12
  /** How `agents run <agent>` chooses an installed version when none is pinned. */
13
13
  export type RunStrategy = 'pinned' | 'available' | 'balanced';
14
14
  export type RunEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';