@phnx-labs/agents-cli 1.22.89 → 1.22.90

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 CHANGED
@@ -1,5 +1,94 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.90
4
+
5
+ - **`accounts migrate --apply` no longer aborts on a worker whose accounts already hold slots (PHNX-3940).** The daemon's auth-sync provisions a durable slot per account on every worker, so `--apply` hit `Slot already exists` on the first credential-bearing legacy home and cleaned nothing. The plan now routes such a home to `trash` (restorable with `agents trash restore`) with the reason "account already holds a provisioned slot on this device", rewrites any `<harness>@<label>` binding to the account, and clears the stale home record; a canonical install in that state keeps its binary, has its stale home moved to `~/.agents/.history/trash/homes/…` (manifest key `<harness>@<label>#home`), and gets an empty home back. A regular file at the slot path is still not a slot, so the apply-time guard still fails loud on that corruption. Source: `cli/src/lib/accounts/migrate.ts`.
6
+
7
+ - **AGI Menu: Sessions window, feed-driven RECENT, Notifications submenu (PHNX-4003).**
8
+ `Cmd-Shift-I` (or the new **Sessions** row at the top of the dropdown) opens a
9
+ persistent Sessions window: live sessions across the fleet grouped by project on
10
+ the left, with **Recent / Needs you / Running / Done today** filter chips, a
11
+ `Cmd-F` search (label, topic, project, ticket, device), and a device strip that
12
+ marks stale heartbeats; a session card on the right shows the phase, PR chip,
13
+ request excerpt, progress tally, latest action, today's rendered artifacts with an
14
+ **Open** button, and the attention block when the agent is waiting — numbered
15
+ option buttons, permission Allow/Deny, plan-review Approve/Send back — answered
16
+ atomically through `agents feed answer`. A reply field routes by capability
17
+ (`agents feed answer`, `agents sessions inject`, `agents cloud message`,
18
+ `agents teams message`) and shows the CLI's own result inline; keys `1`–`9`,
19
+ `Return`, `Cmd-Return`, `Cmd-K` answer, reply, open the terminal, or nudge. The
20
+ dropdown's RECENT section now renders the same feed-driven rows (status dot,
21
+ `n/N` checklist, `working · 18h · host`, latest action + PR chip) grouped by
22
+ project, each opening the window on that session, and a **Notifications**
23
+ submenu lists today's feed banners unanswered-first. Blocked sessions leave the
24
+ NEEDS YOU strip, which keeps only load, scheduler, and routine triage.
25
+ Source: `cli/menubar/Sources/MenubarHelper/SessionsWindow.swift`,
26
+ `cli/menubar/Sources/MenubarHelper/SessionCard.swift`,
27
+ `cli/menubar/Sources/MenubarHelper/SessionRowModel.swift`,
28
+ `cli/menubar/Sources/MenubarHelper/Reply.swift`,
29
+ `cli/menubar/Sources/MenubarHelper/RecentSectionBuilder.swift`.
30
+
31
+ - **AGI Menu delivers attention banners on `UNUserNotificationCenter` with real
32
+ action buttons (PHNX-4004).** The deprecated `NSUserNotification` toast is
33
+ replaced for actionable notices: the banner carries the mapped category, a
34
+ time-sensitive interruption level for a blocked ask, a per-session thread so a
35
+ session's banners group, and the harness avatar as an attachment. The
36
+ persistent menu-bar instance is the delegate; a tapped action runs one bounded
37
+ `agents feed answer` / `agents open` command off the delegate thread and posts
38
+ a "Could not deliver your reply" follow-up when it fails, or a "Could not open
39
+ the report/PR" follow-up when a done banner carried no link for that button.
40
+ When macOS declines notification authorization it degrades to the old plain
41
+ banner so a notice is never lost.
42
+ Source: `menubar/Sources/MenubarHelper/Notifier.swift`,
43
+ `menubar/Sources/MenubarHelper/AgentAvatar.swift`.
44
+
45
+ - **AGI Menu palette: dispatch form with remembered defaults, project pulse, and session-id minting (PHNX-4005).**
46
+ The `Cmd-Shift-O` palette now carries the dimensions a dispatch actually needs
47
+ below its text field — **mode** (Plan / Auto / Edit), **run on** (This Mac,
48
+ `auto`, or a fleet device by name), **surface** (Interactive / Headless), and
49
+ **watchdog** (Off / Keep moving / Hands-off) — collapsed to one summary line
50
+ (`Claude 2.1.263 · this-mac · agi · Auto · Interactive · Keep moving`) with a
51
+ `▸ details` disclosure. Every dimension is **remembered per project**, so
52
+ switching projects restores the agent, mode, and target you last used there and
53
+ one keystroke dispatches. `Cmd-P` switches **this one dispatch** to Plan without
54
+ touching the remembered defaults. A Claude dispatch mints its session id and
55
+ passes `--session-id`, which lets the palette apply a non-default watchdog policy
56
+ at dispatch and register a **launching placeholder** with the feed stream: the
57
+ row shows in the project pulse strip as `◌ launching · <name>` (and in any
58
+ window that reads the feed rows) until the real session reports, and reads
59
+ "did not start" — with the CLI's own stderr — if the launch exits non-zero or
60
+ no row arrives within 60 s. Under the project row a **pulse strip** shows the
61
+ headline milestone from `agents projects status`, open tickets and PRs, live
62
+ running / need-you / idle counts with NOW chips (not-progressing first), and
63
+ the NEXT UP ticket (`Cmd-N` attaches).
64
+ Source: `cli/menubar/Sources/MenubarHelper/DispatchForm.swift`,
65
+ `cli/menubar/Sources/MenubarHelper/DispatchDefaults.swift`,
66
+ `cli/menubar/Sources/MenubarHelper/ProjectPulse.swift`,
67
+ `cli/menubar/Sources/MenubarHelper/FeedStream.swift`,
68
+ `cli/menubar/Sources/MenubarHelper/AgentsCLI.swift`.
69
+
70
+ - **AGI Menu palette: find a screenshot by the text on it (PHNX-4006).**
71
+ Every capture in the palette's screenshot folders (the system `screencapture`
72
+ location, CleanShot's export path, and the clip attachments dir) is now OCR'd
73
+ once, on-device, and indexed so you can search screenshots by the words on them.
74
+ With the palette key, `Cmd-Shift-F` focuses a search field above the strip:
75
+ typing filters the thumbnails to captures whose recognized text matches every
76
+ token (case-insensitive, tokenized AND), newest first, with an `n of m match`
77
+ hint; clearing it restores the newest-six strip. A **more** disclosure expands
78
+ the strip into a grid grouped by hour (last 24 h) then day — each cell shows the
79
+ thumbnail, the time, and the first recognized line — where a click attaches and a
80
+ double-click previews the full image in Preview.app. Nothing leaves the machine:
81
+ recognition runs through Vision locally (`.fast`, no language correction) on a
82
+ background queue, images over 20 MB are skipped and the rest downsampled to
83
+ 2000 px before recognition, each capture is keyed by the SHA-256 of its bytes so
84
+ a rename or duplicate is never OCR'd twice, a deleted capture drops out of the
85
+ index (search and grid never return it, and the next scan prunes its row), and
86
+ the index is a local SQLite file at `~/.agents/.history/menubar/screenshots.db`.
87
+ Source:
88
+ `cli/menubar/Sources/MenubarHelper/ScreenshotIndex.swift`,
89
+ `cli/menubar/Sources/MenubarHelper/ScreenshotGrid.swift`,
90
+ `cli/menubar/Sources/MenubarHelper/PromptPanel.swift`.
91
+
3
92
  ## 1.22.89
4
93
 
5
94
  - **Worker daemons now actually relaunch onto a new agents-cli release, so a fix
@@ -163,6 +163,30 @@ async function duplicateTrashLabels(items) {
163
163
  function plannedTrashPath(agent, label) {
164
164
  return path.join(getHistoryDir(), 'trash', 'versions', agent, label, '<stamp>');
165
165
  }
166
+ /**
167
+ * Where a canonical install's stale `home/` goes when its account already holds
168
+ * a slot: the binary and the (recreated, empty) home stay under `versions/`, so
169
+ * the old home cannot use the versions trash — `agents trash restore` would put a
170
+ * whole install back over a live one. Restoring is a plain move back; the
171
+ * migration manifest records the path under `<agent>@<label>#home`.
172
+ */
173
+ function homeTrashDir(agent, label) {
174
+ return path.join(getHistoryDir(), 'trash', 'homes', agent, label);
175
+ }
176
+ function plannedHomeTrashPath(agent, label) {
177
+ return path.join(homeTrashDir(agent, label), '<stamp>');
178
+ }
179
+ function trashHome(agent, item) {
180
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
181
+ const dest = path.join(homeTrashDir(agent, item.label), stamp);
182
+ fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
183
+ if (!fs.existsSync(item.home)) {
184
+ throw new Error(`Home ${item.home} is missing; cannot trash it.`);
185
+ }
186
+ fs.renameSync(item.home, dest);
187
+ fs.mkdirSync(item.home, { recursive: true, mode: 0o700 });
188
+ return dest;
189
+ }
166
190
  function emptyDir(dir) {
167
191
  if (!fs.existsSync(dir))
168
192
  return true;
@@ -180,6 +204,23 @@ function assertSlotAbsent(agent, accountId) {
180
204
  }
181
205
  return dest;
182
206
  }
207
+ /**
208
+ * True when this device already holds a populated slot directory for the
209
+ * account. The daemon's auth-sync provisions durable worker slots on its own
210
+ * (PHNX-3940), so by the time an operator runs `--apply` a worker commonly has
211
+ * a slot for every account whose legacy per-version home is still on disk.
212
+ * A regular file at the slot path is NOT a slot: the plan keeps routing such a
213
+ * home to `slot` so the apply-time guard fails loud on the corruption.
214
+ */
215
+ function provisionedSlotExists(agent, accountId) {
216
+ const dest = slotDir(agent, accountId);
217
+ try {
218
+ return fs.statSync(dest).isDirectory() && fs.readdirSync(dest).length > 0;
219
+ }
220
+ catch {
221
+ return false;
222
+ }
223
+ }
183
224
  function takenNames(meta) {
184
225
  const taken = new Set();
185
226
  for (const row of listNativeAccounts(meta)) {
@@ -255,6 +296,42 @@ async function planHarness(agent, deps) {
255
296
  });
256
297
  continue;
257
298
  }
299
+ if (item.hasCredential && item.accountId && provisionedSlotExists(agent, item.accountId)) {
300
+ // The account already owns a populated slot on this device, so this
301
+ // per-version home is a stale copy of a credential the slot now carries.
302
+ // Routing it to `slot` would only trip `assertSlotAbsent` and abort the
303
+ // whole apply with nothing done — the state every auth-synced worker was
304
+ // in. Trash it instead (`agents trash restore` reverses). The canonical
305
+ // install keeps its binary and ends up with an empty home, exactly as it
306
+ // does when its home is the one that moves into the slot: two on-disk
307
+ // copies of one credential is not an end state the migration leaves.
308
+ if (canonical && item.label === canonical.label) {
309
+ actions.push({
310
+ kind: 'canonical',
311
+ label: item.label,
312
+ release: item.release,
313
+ reason: 'canonical install (binary kept); account already holds a slot — stale home trashed, empty home recreated',
314
+ accountId: item.accountId,
315
+ identityKey: item.identityKey,
316
+ email: item.email,
317
+ sessionCount: item.sessionCount,
318
+ pathMoves: [{ from: item.home, to: plannedHomeTrashPath(agent, item.label) }],
319
+ });
320
+ continue;
321
+ }
322
+ actions.push({
323
+ kind: 'trash',
324
+ label: item.label,
325
+ release: item.release,
326
+ reason: 'account already holds a provisioned slot on this device — stale home trashed',
327
+ accountId: item.accountId,
328
+ identityKey: item.identityKey,
329
+ email: item.email,
330
+ sessionCount: item.sessionCount,
331
+ pathMoves: [{ from: item.dir, to: plannedTrashPath(agent, item.label) }],
332
+ });
333
+ continue;
334
+ }
258
335
  if (item.hasCredential && item.identityKey) {
259
336
  const dest = item.accountId ? slotDir(agent, item.accountId) : slotDir(agent, '<new-account>');
260
337
  actions.push({
@@ -494,6 +571,21 @@ export async function applyAccountMigration(agents = [...ALL_AGENT_IDS], deps =
494
571
  if (kept)
495
572
  labelToAccount.set(item.label, kept);
496
573
  }
574
+ for (const action of h.actions.filter((a) => a.kind === 'canonical' && a.accountId)) {
575
+ // Canonical install whose account already holds a slot: hand the stale
576
+ // home off to the homes trash and leave an empty home behind, mirroring
577
+ // the slot branch's end state for a canonical home.
578
+ const item = byLabel.get(action.label);
579
+ if (!item)
580
+ throw new Error(`Plan named ${h.agent}@${action.label} but inventory has no such install.`);
581
+ const trashPath = trashHome(h.agent, item);
582
+ remaps.push({ from: item.home, to: trashPath });
583
+ labelToAccount.set(item.label, action.accountId);
584
+ movedAccountIds.push(action.accountId);
585
+ entry.trashed.push({ label: item.label, reason: 'stale canonical home — account already holds a provisioned slot', trashPath });
586
+ manifest.map[`${h.agent}@${item.label}#home`] = trashPath;
587
+ persistManifest(manifestPath, manifest);
588
+ }
497
589
  for (const action of trashActions) {
498
590
  const item = byLabel.get(action.label);
499
591
  if (!item)
@@ -504,6 +596,13 @@ export async function applyAccountMigration(agents = [...ALL_AGENT_IDS], deps =
504
596
  removeVersionedAlias(h.agent, item.label);
505
597
  remaps.push({ from: item.dir, to: trashPath });
506
598
  stillPresent.delete(item.label);
599
+ if (action.accountId) {
600
+ // A home trashed because its account already holds a slot: anything
601
+ // bound to this version label follows the account, and the stale
602
+ // home record clears so spawn resolves through the slot alone.
603
+ labelToAccount.set(item.label, action.accountId);
604
+ movedAccountIds.push(action.accountId);
605
+ }
507
606
  entry.trashed.push({ label: item.label, reason: action.reason, trashPath });
508
607
  manifest.map[`${h.agent}@${item.label}`] = trashPath;
509
608
  persistManifest(manifestPath, manifest);
@@ -13,7 +13,7 @@
13
13
  * stored and optionally synced. Interactive mint is Claude-only; every other
14
14
  * harness fails loud with the command that actually provisions it.
15
15
  */
16
- import type { AgentId } from './types.js';
16
+ import type { AgentId, Meta } from './types.js';
17
17
  import { type CredentialAccount } from './account-registry.js';
18
18
  import { type StorableCredentialKind } from './secrets-policy.js';
19
19
  import { type DriveOptions, type PtyDriver } from './fleet/remote-login.js';
@@ -110,6 +110,27 @@ export declare function seedReservedStoreKey(harness: AgentId, kind: StorableCre
110
110
  bundle: string;
111
111
  key: string;
112
112
  };
113
+ export interface AdoptLegacyReservedItemsResult {
114
+ adopted: Array<{
115
+ bundle: string;
116
+ key: string;
117
+ }>;
118
+ errors: Array<{
119
+ bundle: string;
120
+ key: string;
121
+ message: string;
122
+ }>;
123
+ }
124
+ /**
125
+ * Adopt reserved-store keys written by 1.22.84–1.22.89 as bare file items (no
126
+ * bundle record) into the file-backed bundle the daemon push reads. Every
127
+ * `accounts add codex|grok|cursor` from those releases left its worker key in
128
+ * that shape on the laptop, unpushable; the item name is the same in both
129
+ * shapes, so adoption re-seeds the bundle from the raw value in place and
130
+ * nothing already on a worker changes. Idempotent: a key the bundle already
131
+ * carries is skipped. Local data repair only — it never talks to a peer.
132
+ */
133
+ export declare function adoptLegacyReservedStoreItems(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): AdoptLegacyReservedItemsResult;
113
134
  /**
114
135
  * Create or rotate the named provider account that `agents run --account` and
115
136
  * `agents accounts sync` consume. Existing account of a different kind fails
@@ -1,7 +1,7 @@
1
1
  import { resolveAgentName } from './agents.js';
2
- import { AUTH_BUNDLE, claudeAccountTokenKey, isValidClaudeSetupToken, readClaudeAccountEmail, invalidateClaudeSetupTokenCache, } from './claude-account-token.js';
3
- import { addAccount, findAccount, readAccountRegistry, setAccountSecret } from './account-registry.js';
4
- import { bundleBackendSync, bundleExists, bundleExistsSync, keychainRef, pushBundleToHost, readBundleSync, rotateBundleSecretSync, secretsKeychainItem, storeSetSync, writeBundleWithItemsSync, } from './secrets-client.js';
2
+ import { AUTH_BUNDLE, claudeAccountTokenKey, isValidClaudeSetupToken, readClaudeAccountEmail, readReservedCredential, invalidateClaudeSetupTokenCache, } from './claude-account-token.js';
3
+ import { addAccount, findAccount, listNativeAccounts, readAccountRegistry, setAccountSecret } from './account-registry.js';
4
+ import { bundleBackendSync, isSecretsClientError, bundleExists, bundleExistsSync, keychainRef, pushBundleToHost, readBundleSync, rotateBundleSecretSync, secretsKeychainItem, writeBundleWithItemsSync, } from './secrets-client.js';
5
5
  import { assertStorableCredentialKind, reservedStoreName, } from './secrets-policy.js';
6
6
  import { getBinaryPath, getGlobalDefault, getVersionHomePath, listInstalledVersions } from './installations/versions.js';
7
7
  import { shellQuote } from './ssh-exec.js';
@@ -243,13 +243,88 @@ export function seedReservedStoreKey(harness, kind, key, value) {
243
243
  const cleaned = value.trim();
244
244
  if (!cleaned)
245
245
  throw new Error(`Empty ${kind} for reserved store '${name}' key ${key}.`);
246
- // A `__<harness>__` reserved store is written and read as raw FILE-backed items,
247
- // never as a standalone bundle: the standalone's bundle-name validation rejects
248
- // the `__`-wrapped reserved name, so the worker credential rides the raw item
249
- // directly, symmetric with how `readReservedCredential` reads it. A first write
250
- // and a rotation are the same operation — `store.set` overwrites the item value.
251
- storeSetSync('file', secretsKeychainItem(name, key), cleaned);
252
- return { bundle: name, key };
246
+ // A `__<harness>__` reserved store is a real FILE-backed, policy-`never` bundle
247
+ // — the same shape as the legacy `auth` bundle — because the only transport to
248
+ // a worker is the ordinary bundle push (`syncReservedStores` →
249
+ // `pushBundleToHost`), which reads the store as a bundle and imports it
250
+ // remotely as one. It used to be written as a bare `store.set` item with no
251
+ // bundle record (the standalone rejected the `__`-wrapped name at the time), so
252
+ // the push could never read it and no worker received a Cursor/Codex/Grok key.
253
+ // The item name is unchanged (`agents-cli.secrets.__<harness>__.<KEY>`), so
254
+ // `readReservedCredential` and every worker that already holds the raw item
255
+ // keep resolving it. Requires -labs/secrets-cli >= 0.1.1 (reserved-shape
256
+ // bundle names).
257
+ const item = secretsKeychainItem(name, key);
258
+ try {
259
+ if (bundleExistsSync(name)) {
260
+ const backend = bundleBackendSync(name);
261
+ if (backend !== 'file') {
262
+ throw new Error(`Reserved store '${name}' exists with backend '${backend}', but worker provisioning only reads a FILE-backed store. Recreate it with: agents secrets delete ${name} --yes`);
263
+ }
264
+ const bundle = readBundleSync(name);
265
+ if (key in bundle.vars) {
266
+ rotateBundleSecretSync(bundle, key, { newValue: cleaned, meta: { type: 'token' } });
267
+ return { bundle: name, key };
268
+ }
269
+ bundle.vars[key] = keychainRef(key);
270
+ if (!bundle.meta)
271
+ bundle.meta = {};
272
+ bundle.meta[key] = { type: 'token' };
273
+ writeBundleWithItemsSync(bundle, new Map([[item, cleaned]]));
274
+ return { bundle: name, key };
275
+ }
276
+ const bundle = {
277
+ name,
278
+ backend: 'file',
279
+ policy: 'never',
280
+ description: `Reserved ${harness} worker credentials (${kind}), one key per account; pushed to worker devices by the daemon. Never a native OAuth session.`,
281
+ vars: { [key]: keychainRef(key) },
282
+ meta: { [key]: { type: 'token' } },
283
+ };
284
+ writeBundleWithItemsSync(bundle, new Map([[item, cleaned]]));
285
+ return { bundle: name, key };
286
+ }
287
+ catch (err) {
288
+ // A standalone older than 0.1.1 rejects the `__<harness>__` bundle name and
289
+ // the client only sees a sanitized code. Name the real requirement instead
290
+ // of leaving `accounts add` to fail with `OPERATION_FAILED`.
291
+ if (isSecretsClientError(err, 'OPERATION_FAILED')) {
292
+ throw new Error(`Could not write the reserved store '${name}' as a bundle: ${err.message}. agents-cli needs @phnx-labs/secrets-cli 0.1.1 or newer, which accepts the __<harness>__ bundle name: npm i -g @phnx-labs/secrets-cli@latest`);
293
+ }
294
+ throw err;
295
+ }
296
+ }
297
+ /**
298
+ * Adopt reserved-store keys written by 1.22.84–1.22.89 as bare file items (no
299
+ * bundle record) into the file-backed bundle the daemon push reads. Every
300
+ * `accounts add codex|grok|cursor` from those releases left its worker key in
301
+ * that shape on the laptop, unpushable; the item name is the same in both
302
+ * shapes, so adoption re-seeds the bundle from the raw value in place and
303
+ * nothing already on a worker changes. Idempotent: a key the bundle already
304
+ * carries is skipped. Local data repair only — it never talks to a peer.
305
+ */
306
+ export function adoptLegacyReservedStoreItems(meta) {
307
+ const result = { adopted: [], errors: [] };
308
+ for (const account of listNativeAccounts(meta)) {
309
+ const cred = account.workerCredential;
310
+ if (!cred || !cred.bundle.startsWith('__'))
311
+ continue;
312
+ if (cred.kind !== 'setup-token' && cred.kind !== 'api-key')
313
+ continue;
314
+ try {
315
+ if (bundleExistsSync(cred.bundle) && cred.key in readBundleSync(cred.bundle).vars)
316
+ continue;
317
+ const value = readReservedCredential(cred.bundle, cred.key);
318
+ if (value === null)
319
+ continue;
320
+ seedReservedStoreKey(account.agent, cred.kind, cred.key, value);
321
+ result.adopted.push({ bundle: cred.bundle, key: cred.key });
322
+ }
323
+ catch (err) {
324
+ result.errors.push({ bundle: cred.bundle, key: cred.key, message: err.message });
325
+ }
326
+ }
327
+ return result;
253
328
  }
254
329
  /**
255
330
  * Create or rotate the named provider account that `agents run --account` and
@@ -75,6 +75,8 @@ export class AuthSyncService extends BasePeriodicService {
75
75
  // the only transport — provisioning (above) writes only locally (invariant 1).
76
76
  try {
77
77
  const stores = await syncReservedStores();
78
+ if (stores.adopted.length > 0)
79
+ ctx.log('INFO', `auth-sync: adopted ${stores.adopted.length} legacy reserved item(s) into their bundle: ${stores.adopted.map((a) => `${a.bundle} ${a.key}`).join(', ')}`);
78
80
  for (const p of stores.pushed)
79
81
  ctx.log('INFO', `auth-sync: pushed ${p.bundle} (${p.keys.length} key(s)) to ${p.device}`);
80
82
  for (const err of stores.errors)
@@ -171,6 +171,11 @@ export declare function readDeliveryMemo(root?: string): Record<string, string>;
171
171
  export declare function peerPresentKeys(peer: string, accounts: ReservedSyncAccount[], memo: Record<string, string>, legacyAuthReady: boolean): Record<string, ReadonlySet<string>>;
172
172
  export interface ReservedStoreSyncResult {
173
173
  publisher: string | null;
174
+ /** Legacy raw reserved items adopted into their bundle on this box before planning (local repair). */
175
+ adopted: Array<{
176
+ bundle: string;
177
+ key: string;
178
+ }>;
174
179
  pushed: Array<{
175
180
  device: string;
176
181
  bundle: string;
@@ -197,6 +202,18 @@ export interface ReservedStoreSyncDeps {
197
202
  localReady?: boolean;
198
203
  /** Does THIS publisher hold (bundle, key) to push? Defaults to a local bundle read. */
199
204
  hasLocalKey?: (bundle: string, key: string) => boolean;
205
+ /** Adopt pre-bundle raw reserved items locally before planning; defaults to `adoptLegacyReservedStoreItems`. */
206
+ adoptLegacy?: (meta: Pick<Meta, 'accounts' | 'deviceAccounts'>) => Promise<{
207
+ adopted: Array<{
208
+ bundle: string;
209
+ key: string;
210
+ }>;
211
+ errors: Array<{
212
+ bundle: string;
213
+ key: string;
214
+ message: string;
215
+ }>;
216
+ }>;
200
217
  push?: (bundle: string, host: string) => Promise<PushBundleResult>;
201
218
  sshTarget?: (device: DeviceProfile) => string;
202
219
  }
@@ -375,11 +375,24 @@ function defaultHasLocalKey(bundle, key) {
375
375
  * there is no second scheduler. The daemon runs this each tick.
376
376
  */
377
377
  export async function syncReservedStores(deps = {}) {
378
- const result = { publisher: null, pushed: [], skipped: [], errors: [] };
378
+ const result = { publisher: null, adopted: [], pushed: [], skipped: [], errors: [] };
379
379
  const localName = deps.localName ?? machineId();
380
380
  const localNorm = normalizeHost(localName);
381
381
  const root = deps.userAgentsDir ?? getUserAgentsDir();
382
382
  const meta = (deps.readMetaFn ?? readMeta)();
383
+ // Local repair first: a reserved key written by 1.22.84–1.22.89 is a bare
384
+ // file item with no bundle record, which the push below cannot read. Adopt
385
+ // it into its bundle here so the plan sees it; nothing leaves the box.
386
+ const adopt = deps.adoptLegacy ?? (async (m) => (await import('./auth-mint.js')).adoptLegacyReservedStoreItems(m));
387
+ try {
388
+ const adoption = await adopt(meta);
389
+ result.adopted.push(...adoption.adopted);
390
+ for (const err of adoption.errors)
391
+ result.errors.push({ device: localName, message: `adopt ${err.bundle} ${err.key}: ${err.message}` });
392
+ }
393
+ catch (err) {
394
+ result.errors.push({ device: localName, message: `adopt legacy reserved items: ${err.message}` });
395
+ }
383
396
  const hasLocalKey = deps.hasLocalKey ?? defaultHasLocalKey;
384
397
  const targets = reservedSyncTargets(meta).filter((t) => hasLocalKey(t.bundle, t.key));
385
398
  const devices = (deps.listDevices ?? defaultDevices)().filter((d) => normalizeHost(d.name) !== localNorm);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.89",
3
+ "version": "1.22.90",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",