@songsid/agend 2.3.2-beta.3 → 2.3.2-beta.5

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 (223) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +5 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/antigravity-usage-scan.d.ts +58 -0
  7. package/dist/antigravity-usage-scan.js +257 -0
  8. package/dist/antigravity-usage-scan.js.map +1 -0
  9. package/dist/antigravity-usage-worker.d.ts +1 -0
  10. package/dist/antigravity-usage-worker.js +10 -0
  11. package/dist/antigravity-usage-worker.js.map +1 -0
  12. package/dist/auth-api.js +2 -0
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/kiro-identity.d.ts +45 -0
  15. package/dist/backend/kiro-identity.js +74 -0
  16. package/dist/backend/kiro-identity.js.map +1 -1
  17. package/dist/backend/kiro.d.ts +36 -1
  18. package/dist/backend/kiro.js +131 -6
  19. package/dist/backend/kiro.js.map +1 -1
  20. package/dist/backend/types.d.ts +6 -0
  21. package/dist/backend/types.js.map +1 -1
  22. package/dist/backend-signin-guide.d.ts +1 -0
  23. package/dist/backend-signin-guide.js +9 -0
  24. package/dist/backend-signin-guide.js.map +1 -0
  25. package/dist/backend-signin-pending.d.ts +10 -0
  26. package/dist/backend-signin-pending.js +44 -0
  27. package/dist/backend-signin-pending.js.map +1 -0
  28. package/dist/browser-backend-signins.d.ts +56 -0
  29. package/dist/browser-backend-signins.js +189 -0
  30. package/dist/browser-backend-signins.js.map +1 -0
  31. package/dist/cache-ledger.d.ts +38 -2
  32. package/dist/cache-ledger.js +133 -8
  33. package/dist/cache-ledger.js.map +1 -1
  34. package/dist/cache-prices.d.ts +2 -1
  35. package/dist/cache-prices.js +5 -2
  36. package/dist/cache-prices.js.map +1 -1
  37. package/dist/cache-service.d.ts +68 -0
  38. package/dist/cache-service.js +355 -13
  39. package/dist/cache-service.js.map +1 -1
  40. package/dist/channel/adapters/discord.js +7 -4
  41. package/dist/channel/adapters/discord.js.map +1 -1
  42. package/dist/channel/mcp-tools.js +7 -1
  43. package/dist/channel/mcp-tools.js.map +1 -1
  44. package/dist/classic-channel-manager.d.ts +5 -0
  45. package/dist/classic-channel-manager.js +14 -1
  46. package/dist/classic-channel-manager.js.map +1 -1
  47. package/dist/cli.js +21 -2
  48. package/dist/cli.js.map +1 -1
  49. package/dist/config-validator.js +22 -5
  50. package/dist/config-validator.js.map +1 -1
  51. package/dist/config.d.ts +19 -0
  52. package/dist/config.js +31 -1
  53. package/dist/config.js.map +1 -1
  54. package/dist/cpu-quota.d.ts +23 -0
  55. package/dist/cpu-quota.js +86 -0
  56. package/dist/cpu-quota.js.map +1 -0
  57. package/dist/daemon.js +22 -7
  58. package/dist/daemon.js.map +1 -1
  59. package/dist/dashboard-permissions.d.ts +20 -6
  60. package/dist/dashboard-permissions.js +58 -9
  61. package/dist/dashboard-permissions.js.map +1 -1
  62. package/dist/fleet-manager.d.ts +51 -1
  63. package/dist/fleet-manager.js +416 -32
  64. package/dist/fleet-manager.js.map +1 -1
  65. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  66. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  67. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +24 -1
  68. package/dist/general-knowledge/skills/web-appearance/SKILL.md +24 -1
  69. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +6 -3
  70. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +12 -5
  71. package/dist/instance-config-impact.js +10 -5
  72. package/dist/instance-config-impact.js.map +1 -1
  73. package/dist/instance-lifecycle.d.ts +17 -0
  74. package/dist/instance-lifecycle.js +32 -4
  75. package/dist/instance-lifecycle.js.map +1 -1
  76. package/dist/kiro-db-reader.d.ts +5 -0
  77. package/dist/kiro-db-reader.js +18 -0
  78. package/dist/kiro-db-reader.js.map +1 -1
  79. package/dist/kiro-engine-status.d.ts +2 -0
  80. package/dist/kiro-engine-status.js +9 -0
  81. package/dist/kiro-engine-status.js.map +1 -1
  82. package/dist/kiro-migrate.d.ts +49 -0
  83. package/dist/kiro-migrate.js +105 -0
  84. package/dist/kiro-migrate.js.map +1 -0
  85. package/dist/kiro-transcript-lane.d.ts +2 -0
  86. package/dist/kiro-transcript-lane.js.map +1 -1
  87. package/dist/kiro-transcript-worker.js +1 -0
  88. package/dist/kiro-transcript-worker.js.map +1 -1
  89. package/dist/kiro-usage-run.d.ts +7 -0
  90. package/dist/kiro-usage-run.js +39 -0
  91. package/dist/kiro-usage-run.js.map +1 -0
  92. package/dist/kiro-usage-scan.d.ts +49 -0
  93. package/dist/kiro-usage-scan.js +223 -0
  94. package/dist/kiro-usage-scan.js.map +1 -0
  95. package/dist/kiro-usage-worker.d.ts +1 -0
  96. package/dist/kiro-usage-worker.js +10 -0
  97. package/dist/kiro-usage-worker.js.map +1 -0
  98. package/dist/kiro-usage.d.ts +58 -0
  99. package/dist/kiro-usage.js +310 -0
  100. package/dist/kiro-usage.js.map +1 -0
  101. package/dist/locale.js +72 -8
  102. package/dist/locale.js.map +1 -1
  103. package/dist/log-service.js +1 -1
  104. package/dist/log-service.js.map +1 -1
  105. package/dist/login-url.d.ts +4 -0
  106. package/dist/login-url.js +22 -0
  107. package/dist/login-url.js.map +1 -0
  108. package/dist/needs-you-hub.d.ts +2 -1
  109. package/dist/needs-you-hub.js +1 -1
  110. package/dist/needs-you-hub.js.map +1 -1
  111. package/dist/needs-you.d.ts +8 -2
  112. package/dist/needs-you.js +6 -0
  113. package/dist/needs-you.js.map +1 -1
  114. package/dist/notice-delivery.d.ts +15 -0
  115. package/dist/notice-delivery.js +37 -0
  116. package/dist/notice-delivery.js.map +1 -0
  117. package/dist/opencode-usage-scan.d.ts +28 -0
  118. package/dist/opencode-usage-scan.js +92 -0
  119. package/dist/opencode-usage-scan.js.map +1 -0
  120. package/dist/opencode-usage-worker.d.ts +1 -0
  121. package/dist/opencode-usage-worker.js +10 -0
  122. package/dist/opencode-usage-worker.js.map +1 -0
  123. package/dist/outbound-handlers.js +22 -2
  124. package/dist/outbound-handlers.js.map +1 -1
  125. package/dist/outbound-schemas.d.ts +24 -0
  126. package/dist/outbound-schemas.js +21 -0
  127. package/dist/outbound-schemas.js.map +1 -1
  128. package/dist/public-web-gateway.js +1 -1
  129. package/dist/public-web-gateway.js.map +1 -1
  130. package/dist/quickstart-api.js +13 -2
  131. package/dist/quickstart-api.js.map +1 -1
  132. package/dist/quickstart.js +4 -1
  133. package/dist/quickstart.js.map +1 -1
  134. package/dist/reply-triggers.d.ts +2 -0
  135. package/dist/reply-triggers.js +3 -0
  136. package/dist/reply-triggers.js.map +1 -0
  137. package/dist/settings-api.js +27 -4
  138. package/dist/settings-api.js.map +1 -1
  139. package/dist/settings-change.js +17 -14
  140. package/dist/settings-change.js.map +1 -1
  141. package/dist/settings-effect.js +7 -1
  142. package/dist/settings-effect.js.map +1 -1
  143. package/dist/settings-instance-patch.js +4 -1
  144. package/dist/settings-instance-patch.js.map +1 -1
  145. package/dist/setup-form.d.ts +1 -1
  146. package/dist/setup-form.js +7 -1
  147. package/dist/setup-form.js.map +1 -1
  148. package/dist/setup-host.d.ts +9 -0
  149. package/dist/setup-host.js +126 -6
  150. package/dist/setup-host.js.map +1 -1
  151. package/dist/setup-signin-assets.d.ts +2 -0
  152. package/dist/setup-signin-assets.js +6 -0
  153. package/dist/setup-signin-assets.js.map +1 -0
  154. package/dist/setup-wizard.js +4 -0
  155. package/dist/setup-wizard.js.map +1 -1
  156. package/dist/structured-signin-transport.d.ts +8 -0
  157. package/dist/structured-signin-transport.js +79 -0
  158. package/dist/structured-signin-transport.js.map +1 -0
  159. package/dist/structured-signin.d.ts +131 -0
  160. package/dist/structured-signin.js +388 -0
  161. package/dist/structured-signin.js.map +1 -0
  162. package/dist/tool-permissions.js +12 -0
  163. package/dist/tool-permissions.js.map +1 -1
  164. package/dist/topic-commands.js +6 -2
  165. package/dist/topic-commands.js.map +1 -1
  166. package/dist/transcript-sources.d.ts +26 -2
  167. package/dist/transcript-sources.js +87 -40
  168. package/dist/transcript-sources.js.map +1 -1
  169. package/dist/turn-reply-guard.d.ts +1 -1
  170. package/dist/turn-reply-guard.js +1 -1
  171. package/dist/turn-reply-guard.js.map +1 -1
  172. package/dist/types.d.ts +19 -8
  173. package/dist/ui/backend-signin.js +63 -10
  174. package/dist/ui/fleet-office-editor.js +10 -6
  175. package/dist/ui/fleet-office-layout.js +34 -28
  176. package/dist/ui/fleet-office-model.js +6 -3
  177. package/dist/ui/fleet-office-overview.js +2 -2
  178. package/dist/ui/fleet-office-pack.d.ts +3 -1
  179. package/dist/ui/fleet-office-pack.js +58 -7
  180. package/dist/ui/fleet-office-skin.js +13 -4
  181. package/dist/ui/fleet-office-store.js +27 -1
  182. package/dist/ui/fleet-office-view.js +5 -4
  183. package/dist/ui/fleet-office-zones.js +155 -6
  184. package/dist/ui/fleet-office.js +169 -41
  185. package/dist/ui/fleet-usage.js +19 -15
  186. package/dist/ui/panel-log.js +2 -2
  187. package/dist/ui/panel-needs.js +4 -4
  188. package/dist/ui/panel-settings.js +32 -10
  189. package/dist/ui/settings-dialogs.js +8 -1
  190. package/dist/ui/settings-model.js +14 -6
  191. package/dist/ui/settings-registry.js +4 -0
  192. package/dist/ui/settings-strings.js +14 -0
  193. package/dist/ui/shared/app-i18n.js +4 -2
  194. package/dist/ui/shared/app-shell.js +14 -6
  195. package/dist/ui/shared/app-stream.js +2 -1
  196. package/dist/ui/shared/app.css +18 -2
  197. package/dist/ui/shared/side-panel.js +95 -0
  198. package/dist/ui/structured-signin.js +115 -0
  199. package/dist/usage-report.d.ts +20 -0
  200. package/dist/usage-report.js +54 -17
  201. package/dist/usage-report.js.map +1 -1
  202. package/dist/usage-rollup.d.ts +9 -1
  203. package/dist/usage-rollup.js +20 -4
  204. package/dist/usage-rollup.js.map +1 -1
  205. package/dist/web-api.d.ts +4 -0
  206. package/dist/web-api.js +110 -0
  207. package/dist/web-api.js.map +1 -1
  208. package/dist/web-auth.js +1 -1
  209. package/dist/web-auth.js.map +1 -1
  210. package/dist/web-backend-login.d.ts +1 -7
  211. package/dist/web-backend-login.js +2 -24
  212. package/dist/web-backend-login.js.map +1 -1
  213. package/dist/web-office-acts.d.ts +173 -0
  214. package/dist/web-office-acts.js +285 -0
  215. package/dist/web-office-acts.js.map +1 -0
  216. package/dist/web-office-skins.d.ts +1 -1
  217. package/dist/web-office-skins.js +1 -1
  218. package/dist/web-office-skins.js.map +1 -1
  219. package/dist/web-settings-patch.d.ts +2 -1
  220. package/dist/web-settings-patch.js +4 -1
  221. package/dist/web-settings-patch.js.map +1 -1
  222. package/package.json +2 -1
  223. package/templates/windows-shortcuts.ps1 +18 -7
@@ -1,3 +1,6 @@
1
+ import { hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
2
+ import { loginNoticeMode, tipNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
3
+ import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
1
4
  import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
2
5
  import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
3
6
  import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
@@ -28,13 +31,16 @@ import { ProfileControlServer } from "./profile-control.js";
28
31
  import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, accessSync, realpathSync, lstatSync, constants as fsConstants, closeSync } from "node:fs";
29
32
  import { atomicWriteFileSync } from "./atomic-write.js";
30
33
  import { createHash, randomBytes, randomUUID } from "node:crypto";
31
- import { freemem, totalmem, cpus, homedir } from "node:os";
34
+ import { freemem, totalmem, homedir } from "node:os";
35
+ import { effectiveCpuCount } from "./cpu-quota.js";
32
36
  import { createServer } from "node:http";
33
37
  import { Worker } from "node:worker_threads";
34
38
  import { ProbeWorkerPool } from "./probe-worker-pool.js";
35
39
  import { join, dirname, basename, delimiter, isAbsolute, relative, resolve as resolvePath } from "node:path";
36
40
  import { fileURLToPath } from "node:url";
41
+ import { isDeepStrictEqual } from "node:util";
37
42
  import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
43
+ import { applyKiroMigration, kiroMigrationPrompt, planKiroMigration } from "./kiro-migrate.js";
38
44
  import { fleetLabel } from "./fleet-label.js";
39
45
  import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
40
46
  import { formatUpdateProgress } from "./update-progress.js";
@@ -53,7 +59,7 @@ const __dirname = dirname(__filename);
53
59
  const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
54
60
  import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, normalizeEmoji, previewStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusEmojiProblem, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
55
61
  import { isProbeableRouteTarget } from "./fleet-context.js";
56
- import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG } from "./config.js";
62
+ import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
57
63
  import { EventLog } from "./event-log.js";
58
64
  import { binaryProbe } from "./binary-probe.js";
59
65
  import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes } from "./backend-availability.js";
@@ -160,6 +166,7 @@ import { OwnerCssStore } from "./web-owner-css-store.js";
160
166
  import { ThemeStore } from "./web-theme-store.js";
161
167
  import { OfficeStore, OfficeWriteLimiter } from "./web-office-store.js";
162
168
  import { OfficeSkinStore } from "./web-office-skins.js";
169
+ import { OfficeActs } from "./web-office-acts.js";
163
170
  import { WebAppearance } from "./web-appearance.js";
164
171
  import { WebEffectLibrary } from "./web-effect-library.js";
165
172
  import { WebEffectTrials } from "./web-effect-trials.js";
@@ -189,7 +196,7 @@ import { discordGeneralChannelId } from "./discord-general.js";
189
196
  import { parseEnvText } from "./env-file.js";
190
197
  import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
191
198
  import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
192
- import { LOGIN_FLOWS, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
199
+ import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
193
200
  import { LoginSession } from "./login-manager.js";
194
201
  import { runOnLoginShell } from "./login-shell.js";
195
202
  import { tightenInstanceDirs } from "./private-dir.js";
@@ -209,6 +216,10 @@ import { admitProfileLogin, LoginController, LOGIN_TOKEN_RESEND_PREFIX, POST_LOG
209
216
  import { WebBackendLogins, dashboardSignInSupported } from "./web-backend-login.js";
210
217
  import { runBeforeDeadline } from "./deadline.js";
211
218
  import { LoginWindowLock } from "./login-window-lock.js";
219
+ import { BrowserBackendSignIns } from "./browser-backend-signins.js";
220
+ import { createStructuredSignInTransport } from "./structured-signin-transport.js";
221
+ import { ONBOARDING_BACKENDS, readPendingBackendSignIns } from "./backend-signin-pending.js";
222
+ import { structuredSignInSupported } from "./structured-signin.js";
212
223
  import { handleSettingsRequest } from "./settings-api.js";
213
224
  import { setLocale, detectLocale, getLocale, t } from "./locale.js";
214
225
  import { recentChatContext } from "./log-tail.js";
@@ -272,6 +283,7 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
272
283
  /** A self-restart is a whole service restart; 120s is the apply budget, not this. */
273
284
  const SELF_RESTART_DEADLINE_MS = 300_000;
274
285
  import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
286
+ import { kiroHome } from "./backend/kiro-kas-store.js";
275
287
  import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
276
288
  import { carryClaudeConversation } from "./backend/claude-carry.js";
277
289
  import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
@@ -469,6 +481,10 @@ const OOM_RETRY_CALLBACK_PREFIX = "oom-retry:";
469
481
  const NEEDS_PAUSE_TTL_MS = 10_000;
470
482
  const HANG_CALLBACK_PREFIX = "hang:";
471
483
  const CLEAR_CONFIRM_CALLBACK_PREFIX = "clear-confirm:";
484
+ /** #2598 B2: a kiro classic → TUI move's Confirm in General. "kiro-migrate:" (13) + 32 hex + ":confirm" (8) = 53 bytes. */
485
+ const KIRO_MIGRATE_CALLBACK_PREFIX = "kiro-migrate:";
486
+ /** Long enough for an admin to read the prompt; the move is judged again on the click. */
487
+ const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
472
488
  /**
473
489
  * The prompts the web dashboard also offers: the ones about an instance's own health, which a dashboard
474
490
  * user — holding the full-fleet web credential — may answer exactly as a fleet admin on the platform may.
@@ -564,6 +580,7 @@ export const CLI_ENV_PROBE_DEADLINE_MS = 16_000;
564
580
  * started ten CLIs together, saturated the CPU, and healthy starts then missed
565
581
  * their startup budget — which used to cost the user their conversation. Cores
566
582
  * bound how many can actually make progress; memory bounds how many fit.
583
+ * `cores` is what this process may use (cpuset and cgroup quota, #749), not the host's count.
567
584
  */
568
585
  export function deriveSpawnConcurrency(freeMemMB, cores) {
569
586
  const byMemory = Math.floor(freeMemMB / 300);
@@ -881,6 +898,8 @@ export class FleetManager {
881
898
  pendingBackendSelects = new Map();
882
899
  /** nonce → pending button prompt (hang restart, interactive assist, clean-exit restart). */
883
900
  pendingNonceButtons = new Map();
901
+ /** #2598 B2: the kiro moves waiting on a fleet admin's Confirm in General, by prompt nonce. */
902
+ pendingKiroMigrations = new Map();
884
903
  /** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
885
904
  needsYou = null;
886
905
  /** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
@@ -1147,6 +1166,53 @@ export class FleetManager {
1147
1166
  officeSkinWrites = new OfficeWriteLimiter(undefined, { perWindow: 10, windowMs: 60_000 });
1148
1167
  /** How often a person may save the offices (PUT /ui/offices); memory only, as the appearance limiter. */
1149
1168
  officeWrites = new OfficeWriteLimiter();
1169
+ officeActsInstance;
1170
+ /** #2578: the agents' acts in the Office view (office_view / office_set), memory only; every change is `office_acts`. */
1171
+ get officeActs() {
1172
+ return (this.officeActsInstance ??= new OfficeActs({
1173
+ offices: () => this.officeStore.list().offices,
1174
+ pack: id => this.officeSkins.pack(id),
1175
+ chart: () => { const c = this.orgChart(); return { names: Object.keys(c.instances), teams: c.teams }; },
1176
+ people: () => this.officePeople(),
1177
+ changed: acts => this.emitSseEvent("office_acts", { acts, now: Date.now() }),
1178
+ }));
1179
+ }
1180
+ /**
1181
+ * #2578: each instance's state as the org chart shows it (fleet-org.js orgState, from the same fields the status
1182
+ * frame and the Needs-you list carry) and its display name (resolveInstanceIdentity, as getUiStatus) — read once.
1183
+ */
1184
+ officePeople() {
1185
+ const needs = new Map();
1186
+ for (const item of this.needsYouItems())
1187
+ needs.set(item.instance, (needs.get(item.instance) ?? 0) + 1);
1188
+ const classicRooms = new Map((this.classicChannels?.getAll() ?? []).map(ch => [ch.instanceName, ch]));
1189
+ let profiles = null;
1190
+ return {
1191
+ stateOf: name => {
1192
+ if (!this.isKnownInstance(name))
1193
+ return "missing";
1194
+ const p = this.instancePresentation(name), status = this.getInstanceStatus(name);
1195
+ if (p.state === "awaiting_input" || (needs.get(name) ?? 0) > 0)
1196
+ return "needs";
1197
+ if (p.startup_phase === "queued" || p.startup_phase === "starting")
1198
+ return p.startup_phase;
1199
+ if (status === "crashed" || status === "paused")
1200
+ return status;
1201
+ if (status !== "running")
1202
+ return "stopped";
1203
+ return p.execution_state === "stuck" || p.execution_state === "working" ? p.execution_state : "idle";
1204
+ },
1205
+ displayName: name => {
1206
+ try {
1207
+ profiles ??= profileIdentities(this.dataDir);
1208
+ return resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[name], classic: classicRooms.get(name), profile: profiles.get(name) }).display_name || undefined;
1209
+ }
1210
+ catch {
1211
+ return undefined;
1212
+ }
1213
+ },
1214
+ };
1215
+ }
1150
1216
  /** #1629: the theme tools, a person's reset and lock, and the one appearance rate limiter (shared with effects). */
1151
1217
  get webAppearance() {
1152
1218
  return (this.webAppearanceService ??= new WebAppearance(this.themeStore, () => this.fleetConfig?.web ?? null, () => { try {
@@ -1672,6 +1738,74 @@ export class FleetManager {
1672
1738
  // No platform confirmation was reachable. Host inspection is still required.
1673
1739
  this.logger.info({ id: view.id }, "Settings change requires local `agend settings confirm <id>`");
1674
1740
  }
1741
+ kiroMigrateDeps() {
1742
+ return {
1743
+ agendHome: getAgendHome(),
1744
+ fleetConfig: () => this.fleetConfig,
1745
+ saveFleetConfig: () => this.saveFleetConfig(),
1746
+ status: name => this.getInstanceStatus(name),
1747
+ restart: name => this.restartSingleInstance(name, { explicit: true }),
1748
+ };
1749
+ }
1750
+ /**
1751
+ * #2598 B2, migrate_kiro_instance: judge the move, then ask in General for a fleet admin's Confirm. Nothing changes
1752
+ * here; the move runs on the Confirm (handleKiroMigrateCallback), judged again then.
1753
+ */
1754
+ async requestKiroMigration(name, startFresh) {
1755
+ const judged = planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
1756
+ if (!judged.ok)
1757
+ return judged;
1758
+ const general = this.findGeneralInstance();
1759
+ const adapterId = general ? this.getInstanceAdapterId(general) : undefined;
1760
+ const adapter = general ? this.getAdapterForInstance(general) : undefined;
1761
+ const chatId = general ? this.getGroupIdForInstance(general) : undefined;
1762
+ const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
1763
+ if (!general || !adapterId || !adapter || !chatId)
1764
+ return { ok: false, error: t("kiro_migrate.no_general") };
1765
+ const { plan } = judged;
1766
+ try {
1767
+ const nonce = await this.postNonceButtonPromptOrThrow({
1768
+ prefix: KIRO_MIGRATE_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: name, adapter, adapterId, chatId,
1769
+ ...(topic ? { threadId: topic } : {}),
1770
+ message: kiroMigrationPrompt(plan, this.getInstanceStatus(name) === "running"),
1771
+ choices: [{ action: "confirm", label: t("kiro_migrate.confirm_button") }, { action: "cancel", label: t("kiro_migrate.cancel_button") }],
1772
+ expiredText: t("kiro_migrate.expired", name),
1773
+ timeoutMs: KIRO_MIGRATE_TIMEOUT_MS,
1774
+ });
1775
+ this.pendingKiroMigrations.set(nonce, plan);
1776
+ const expiry = setTimeout(() => this.pendingKiroMigrations.delete(nonce), KIRO_MIGRATE_TIMEOUT_MS + 60_000);
1777
+ expiry.unref?.();
1778
+ }
1779
+ catch (err) {
1780
+ return { ok: false, error: `${t("kiro_migrate.no_general")} (${err.message})` };
1781
+ }
1782
+ return { ok: true, text: t("kiro_migrate.requested"), classic_conversation: plan.classicId };
1783
+ }
1784
+ /** A fleet admin's Confirm or Cancel on a kiro move. Any fleet admin (the default nonce authorization), in General. */
1785
+ async handleKiroMigrateCallback(data, adapterId, adapter) {
1786
+ const claimed = this.consumeNonceCallback(KIRO_MIGRATE_CALLBACK_PREFIX, /^kiro-migrate:([0-9a-f]{32}):(confirm|cancel)$/, data, adapterId, adapter);
1787
+ if (claimed === null)
1788
+ return false;
1789
+ if (claimed === "consumed")
1790
+ return true;
1791
+ const { entry, action } = claimed;
1792
+ const nonce = data.callbackData.slice(KIRO_MIGRATE_CALLBACK_PREFIX.length, KIRO_MIGRATE_CALLBACK_PREFIX.length + 32);
1793
+ const plan = this.pendingKiroMigrations.get(nonce);
1794
+ this.pendingKiroMigrations.delete(nonce);
1795
+ const messageId = entry.messageId ?? data.messageId;
1796
+ if (action === "cancel" || !plan) {
1797
+ await this.retireNonceButtons(entry, messageId, t(plan ? "kiro_migrate.cancelled" : "kiro_migrate.expired", entry.instanceName));
1798
+ return true;
1799
+ }
1800
+ this.eventLog?.insert(plan.name, "kiro_migrate", { classicId: plan.classicId, userId: data.userId });
1801
+ await this.retireNonceButtons(entry, messageId, t("kiro_migrate.applying", plan.name));
1802
+ const outcome = await applyKiroMigration(this.kiroMigrateDeps(), plan);
1803
+ this.logger.info({ name: plan.name, ok: outcome.ok, classicId: plan.classicId }, "kiro classic → TUI move");
1804
+ this.notifyFleetError(outcome.text);
1805
+ if (outcome.ok)
1806
+ this.notifyInstanceTopic(plan.name, outcome.text);
1807
+ return true;
1808
+ }
1675
1809
  async handleSettingsChangeCallback(data, adapterId, adapter) {
1676
1810
  const result = this.consumeNonceCallback("settings-confirm:", /^settings-confirm:([0-9a-f]{32}):(confirm|reject)$/, data, adapterId, adapter, undefined, (userId, entry) => entry.confirmationAdmin?.(userId) === true);
1677
1811
  if (!result)
@@ -2525,7 +2659,7 @@ export class FleetManager {
2525
2659
  const explicit = this.fleetConfig?.defaults?.startup?.concurrency;
2526
2660
  if (explicit != null)
2527
2661
  return Math.max(1, Math.min(20, explicit));
2528
- return deriveSpawnConcurrency(Math.round(freemem() / (1024 * 1024)), cpus().length);
2662
+ return deriveSpawnConcurrency(Math.round(freemem() / (1024 * 1024)), effectiveCpuCount());
2529
2663
  }
2530
2664
  /** #1685: "elevated" or "critical" while host memory pressure slows or holds new agents; otherwise null (an
2531
2665
  * unknown macOS reading — the advisory case — is unknown, so null too). */
@@ -3308,7 +3442,7 @@ export class FleetManager {
3308
3442
  await data.respond(t("permission.denied"));
3309
3443
  return;
3310
3444
  }
3311
- this.fleetConfig.defaults.tips = mode === "on";
3445
+ setNoticeMode(this.fleetConfig, "tips", mode === "on");
3312
3446
  this.saveFleetConfig();
3313
3447
  await data.respond(t(mode === "on" ? "tips.enabled" : "tips.disabled"));
3314
3448
  return;
@@ -5098,6 +5232,7 @@ export class FleetManager {
5098
5232
  }
5099
5233
  classicBehaviorUpdate(instanceName) {
5100
5234
  return {
5235
+ reply_triggers: this.classicChannels?.getReplyTriggersByInstance(instanceName, this.fleetConfig?.defaults?.reply_triggers) ?? DEFAULT_REPLY_TRIGGERS,
5101
5236
  tool_progress: this.classicChannels?.getToolProgressByInstance(instanceName, this.fleetConfig?.defaults?.tool_progress) ?? "off",
5102
5237
  reply_completion_guard: this.classicChannels?.getReplyCompletionGuardByInstance(instanceName, this.fleetConfig?.defaults?.reply_completion_guard) ?? true,
5103
5238
  };
@@ -6446,11 +6581,33 @@ export class FleetManager {
6446
6581
  getPublicWebStatus() {
6447
6582
  return this.publicWebLink?.status() ?? { state: "closed" };
6448
6583
  }
6584
+ /** Notices use the first explicit admin of the owning connection; never a guest/paired account. */
6585
+ noticeAdmin(adapterId) {
6586
+ return this.adminListOf(adapterId)?.find(id => id.length > 0);
6587
+ }
6588
+ /** Best-effort private delivery. The caller retains its existing topic fallback and authority fences. */
6589
+ async tryNoticeDm(adapter, userId, text) {
6590
+ if (!userId || !adapter?.sendDirect)
6591
+ return false;
6592
+ try {
6593
+ await withinBudget(adapter.sendDirect(userId, text, { disablePreview: true }), performance.now() + 2_000);
6594
+ return true;
6595
+ }
6596
+ catch {
6597
+ this.logger.info({ adapterId: adapter.id }, "Fleet notice DM failed; falling back to the topic");
6598
+ return false;
6599
+ }
6600
+ }
6449
6601
  async confirmPublicWebLogin(info, current) {
6450
6602
  if (!current() || !this.publicOwnerCurrent(info.owner))
6451
6603
  throw new Error("public owner unavailable");
6452
6604
  const adapter = info.owner.binding;
6453
- await adapter.sendText(info.owner.chatId, t("web.public_login_notice", info.label, info.handle.slice(0, 8)), { threadId: info.owner.threadId, allowedMentions: { parse: [] } });
6605
+ const text = t("web.public_login_notice", info.label, info.handle.slice(0, 8));
6606
+ if (loginNoticeMode(this.fleetConfig?.web) !== "dm" || !await this.tryNoticeDm(adapter, info.owner.userId, text)) {
6607
+ if (!current() || !this.publicOwnerCurrent(info.owner))
6608
+ throw new Error("public owner changed");
6609
+ await adapter.sendText(info.owner.chatId, text, { threadId: info.owner.threadId, allowedMentions: { parse: [] } });
6610
+ }
6454
6611
  if (!current() || !this.publicOwnerCurrent(info.owner))
6455
6612
  throw new Error("public owner changed");
6456
6613
  }
@@ -6468,7 +6625,12 @@ export class FleetManager {
6468
6625
  if (!open)
6469
6626
  throw new Error("guest public sign-in unavailable");
6470
6627
  const adapter = open.owner.binding;
6471
- await adapter.sendText(open.owner.chatId, t("web.guest_public_login_notice", this.webPerson(info.account).name, info.label, this.guestScope(info.account).size, info.handle.slice(0, 8)), { threadId: open.owner.threadId, allowedMentions: { parse: [] } });
6628
+ const text = t("web.guest_public_login_notice", this.webPerson(info.account).name, info.label, this.guestScope(info.account).size, info.handle.slice(0, 8));
6629
+ if (loginNoticeMode(this.fleetConfig?.web) !== "dm" || !await this.tryNoticeDm(adapter, open.owner.userId, text)) {
6630
+ if (!admitted())
6631
+ throw new Error("guest public sign-in changed");
6632
+ await adapter.sendText(open.owner.chatId, text, { threadId: open.owner.threadId, allowedMentions: { parse: [] } });
6633
+ }
6472
6634
  if (!admitted())
6473
6635
  throw new Error("guest public sign-in changed");
6474
6636
  }
@@ -7306,17 +7468,26 @@ export class FleetManager {
7306
7468
  }
7307
7469
  /** Called by the sign-in endpoint: a login the operator did not make should be visible to them. */
7308
7470
  onWebLogin(info) {
7309
- // #1920 §2.3: a guest's sign-in is always said in General (web.notify_login is the operator's own sign-ins): who,
7310
- // and how many agents they now reach — admins stay aware of everyone who is not one of them.
7311
- if (info.tier === "guest" && info.owner) {
7312
- this.notifyFleetError(t("web.guest_login_notice", this.webPerson(info.owner).name, info.label, this.guestScope(info.owner).size, info.handle.slice(0, 8)));
7471
+ // Keep the existing false policy: guest and public-link security notices remain mandatory.
7472
+ const guest = info.tier === "guest" && info.owner;
7473
+ if (!guest && this.fleetConfig?.web?.notify_login === false)
7474
+ return;
7475
+ const text = guest
7476
+ ? t("web.guest_login_notice", this.webPerson(info.owner).name, info.label, this.guestScope(info.owner).size, info.handle.slice(0, 8))
7477
+ : t("web.login_notice", info.label, info.surface, info.handle.slice(0, 8));
7478
+ if (loginNoticeMode(this.fleetConfig?.web) === "dm") {
7479
+ const adapterId = info.owner?.adapterId;
7480
+ const adapter = adapterId ? this.adapters.get(adapterId) : undefined;
7481
+ // A local guest's own account must never receive the admin's security notice.
7482
+ const userId = guest ? (adapterId ? this.noticeAdmin(adapterId) : undefined)
7483
+ : info.owner && this.isFleetAdmin(info.owner.userId, adapterId) ? info.owner.userId : undefined;
7484
+ void this.tryNoticeDm(adapter, userId, text).then(sent => { if (!sent)
7485
+ this.notifyFleetError(text); })
7486
+ .catch(() => this.logger.warn("Web sign-in notice could not be delivered"));
7313
7487
  return;
7314
7488
  }
7315
- if (this.fleetConfig?.web?.notify_login === false)
7316
- return;
7317
- // The session handle makes each notice distinct: notifyFleetError throttles by text, and a second
7318
- // sign-in from the same kind of browser is exactly the one the operator most needs to hear about.
7319
- this.notifyFleetError(t("web.login_notice", info.label, info.surface, info.handle.slice(0, 8)));
7489
+ // Exactly the old topic path, including the session-handle throttle key.
7490
+ this.notifyFleetError(text);
7320
7491
  }
7321
7492
  getDashboardAccess() {
7322
7493
  return { ready: this.healthServerListening, token: this.webToken };
@@ -7686,7 +7857,8 @@ export class FleetManager {
7686
7857
  }
7687
7858
  }
7688
7859
  async startAllWithinDeadline(configPath, deadline) {
7689
- this.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
7860
+ if (!this.structuredLoginCleanupFailed)
7861
+ this.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
7690
7862
  this.loginController?.reopen();
7691
7863
  const startupStartedAt = Date.now();
7692
7864
  FleetManager.signalTarget = this;
@@ -7947,12 +8119,7 @@ export class FleetManager {
7947
8119
  mkdirSync(generalDir, { recursive: true, mode: 0o700 });
7948
8120
  const backendName = fleet.defaults.backend ?? "claude-code";
7949
8121
  this.ensureGeneralInstructions(generalDir, backendName, name);
7950
- fleet.instances[name] = {
7951
- ...DEFAULT_INSTANCE_CONFIG,
7952
- working_directory: generalDir,
7953
- general_topic: true,
7954
- channel_id: need.adapterId,
7955
- };
8122
+ fleet.instances[name] = autoGeneralInstanceConfig(fleet.defaults, generalDir, need.adapterId); // #2598: its kiro_ui is the fleet's
7956
8123
  generalsCreated = true;
7957
8124
  }
7958
8125
  if (generalsCreated)
@@ -11122,7 +11289,7 @@ export class FleetManager {
11122
11289
  if (!ipc || !fleetRequestId)
11123
11290
  return;
11124
11291
  const type = String(msg.type);
11125
- const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) ? "fleet_web_theme_response"
11292
+ const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) || type === "fleet_office_view" || type === "fleet_office_set" ? "fleet_web_theme_response"
11126
11293
  : type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
11127
11294
  : type.startsWith("fleet_decision_") ? "fleet_decision_response"
11128
11295
  : type === "fleet_task" ? "fleet_task_response"
@@ -11176,7 +11343,8 @@ export class FleetManager {
11176
11343
  || type === "fleet_get_web_appearance" || type === "fleet_preview_web_appearance"
11177
11344
  || type === "fleet_set_web_widget" || type === "fleet_remove_web_widget"
11178
11345
  || type === "fleet_set_web_ambient" || type === "fleet_clear_web_ambient"
11179
- || type === "fleet_export_web_mod" || type === "fleet_propose_web_mod") {
11346
+ || type === "fleet_export_web_mod" || type === "fleet_propose_web_mod"
11347
+ || type === "fleet_office_view" || type === "fleet_office_set") {
11180
11348
  this.handleWebTheme(name, msg);
11181
11349
  return;
11182
11350
  }
@@ -11197,7 +11365,8 @@ export class FleetManager {
11197
11365
  const result = tool === "get_web_appearance" || tool === "preview_web_appearance"
11198
11366
  ? this.appearanceTools.handle(instanceName, tool, args)
11199
11367
  : tool === "export_web_mod" || tool === "propose_web_mod" ? this.agentMods.handle(instanceName, tool, args)
11200
- : this.webAppearance.handleTool(instanceName, tool, args);
11368
+ : tool === "office_view" || tool === "office_set" ? this.officeActs.handle(instanceName, tool, args)
11369
+ : this.webAppearance.handleTool(instanceName, tool, args);
11201
11370
  ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
11202
11371
  }
11203
11372
  catch (err) {
@@ -11633,6 +11802,8 @@ export class FleetManager {
11633
11802
  return this.appearanceTools.handle(instance, tool, args);
11634
11803
  if (tool === "export_web_mod" || tool === "propose_web_mod")
11635
11804
  return this.agentMods.handle(instance, tool, args);
11805
+ if (tool === "office_view" || tool === "office_set")
11806
+ return this.officeActs.handle(instance, tool, args); // #2578
11636
11807
  return this.webAppearance.handleTool(instance, tool, args);
11637
11808
  }
11638
11809
  async handlePreviewEmojisHttp(instance, args) {
@@ -12067,6 +12238,23 @@ export class FleetManager {
12067
12238
  return;
12068
12239
  if (!this.savedFleetConfigSnapshot)
12069
12240
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
12241
+ normalizeNoticeDelivery(this.fleetConfig);
12242
+ normalizeOfficeStorage(this.fleetConfig);
12243
+ explicitPatches = explicitPatches.flatMap(patch => {
12244
+ const path = patch.path;
12245
+ if (path.at(-2) === "dashboard" && path.at(-1) === "office" && !patch.remove) {
12246
+ return [{ ...patch, path: [...path.slice(0, -2), "office_access"] }, { path, remove: true }];
12247
+ }
12248
+ if (path.at(-1) === "dashboard" && patch.value && typeof patch.value === "object" && Object.hasOwn(patch.value, "office")) {
12249
+ const { office, ...dashboard } = patch.value;
12250
+ return [{ ...patch, value: dashboard }, { path: [...path.slice(0, -1), "office_access"], value: office, remove: office === null }];
12251
+ }
12252
+ if (patch.path.join(".") === "web.notify_login" && patch.value === "dm")
12253
+ return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "login"], value: "dm" }];
12254
+ if (patch.path.join(".") === "defaults.tips" && patch.value === "dm")
12255
+ return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "tips"], value: "dm" }];
12256
+ return [patch];
12257
+ });
12070
12258
  // Re-read immediately before patching so an unrelated concurrent/manual
12071
12259
  // edit is retained. Invalid concurrent YAML is never overwritten.
12072
12260
  const source = existsSync(this.configPath) ? readFileSync(this.configPath, "utf-8") : "{}\n";
@@ -12142,9 +12330,46 @@ export class FleetManager {
12142
12330
  },
12143
12331
  });
12144
12332
  this.rawFleetConfig = loadRawFleetConfig(this.configPath);
12333
+ // The instance map is defaults-expanded. A defaults edit changes that view even when it writes no instance
12334
+ // leaves. Re-resolve from the committed YAML so inherited values cannot become stale overrides on the next save.
12335
+ // Keep the FleetConfig object itself: Settings and other request contexts hold it.
12336
+ const resolvedInstances = loadFleetConfig(this.configPath).instances;
12337
+ for (const [name, resolved] of Object.entries(resolvedInstances)) {
12338
+ const current = this.fleetConfig.instances[name];
12339
+ if (!current) {
12340
+ this.fleetConfig.instances[name] = resolved;
12341
+ continue;
12342
+ }
12343
+ // Lifecycle rollback fences hold these object identities across save. Refresh their values, not their owner.
12344
+ for (const key of Object.keys(current))
12345
+ if (!Object.hasOwn(resolved, key))
12346
+ delete current[key];
12347
+ Object.assign(current, resolved);
12348
+ }
12145
12349
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
12350
+ this.refreshSavedDashboardPermissions();
12146
12351
  this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
12147
12352
  }
12353
+ /** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
12354
+ refreshSavedDashboardPermissions() {
12355
+ if (!this.fleetConfig)
12356
+ return;
12357
+ const classicNames = new Set(this.classicChannels?.getAll().map(ch => ch.instanceName) ?? []);
12358
+ for (const [name, daemon] of this.daemons) {
12359
+ const next = this.fleetConfig.instances[name] ?? (classicNames.has(name) ? getEffectiveInstanceDefaults(this.fleetConfig.defaults) : undefined);
12360
+ const previous = daemon.getConfigSnapshot?.();
12361
+ if (!next || !previous)
12362
+ continue;
12363
+ if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access)
12364
+ continue;
12365
+ // Only permissions are adopted here. Cold fields retain the live daemon's old snapshot, so reconcile still
12366
+ // detects that they require a restart. MCP menus also watch the raw file; this updates the next spawn's env.
12367
+ daemon.applyConfigUpdate({ dashboard: next.dashboard ?? null, office_access: next.office_access ?? null });
12368
+ if (taskBoardSwitchedOn(previous.dashboard, next.dashboard)) {
12369
+ this.admitTaskBoardNotice(name, TASK_BOARD_ON_NOTICE, `task-board-on:${name}:${Date.now()}`);
12370
+ }
12371
+ }
12372
+ }
12148
12373
  /** Why the config written to `candidate` must not replace fleet.yaml, or null: it does not load, or it adds errors. */
12149
12374
  savedFleetConfigProblem(candidate) {
12150
12375
  let after;
@@ -12182,7 +12407,43 @@ export class FleetManager {
12182
12407
  this.logger.warn({ path: w.path }, `fleet.yaml: ${w.path ? `${w.path}: ` : ""}${w.message}`);
12183
12408
  }
12184
12409
  /** One-time upgrade migration; invalid YAML is never rewritten. */
12410
+ migrateNoticeDeliveryStorage() {
12411
+ if (this.fleetConfig && this.configPath && (this.fleetConfig.web?.notify_login === "dm" || this.fleetConfig.defaults.tips === "dm")
12412
+ && validateFleetConfig(this.rawFleetConfig).valid) {
12413
+ try {
12414
+ this.writeFleetConfigBackup(readFileSync(this.configPath, "utf8"));
12415
+ this.saveFleetConfig();
12416
+ this.logger.info("Stored DM notice routing with downgrade-safe boolean fields");
12417
+ }
12418
+ catch (err) {
12419
+ this.logger.warn({ err }, "Could not migrate DM notice routing; keeping the original file");
12420
+ }
12421
+ }
12422
+ }
12423
+ migrateOfficeStorage() {
12424
+ if (!this.fleetConfig || !this.configPath || !hasLegacyOffice(this.rawFleetConfig) || !validateFleetConfig(this.rawFleetConfig).valid)
12425
+ return;
12426
+ try {
12427
+ this.writeFleetConfigBackup(readFileSync(this.configPath, "utf8"));
12428
+ const patches = [];
12429
+ const raw = this.rawFleetConfig;
12430
+ for (const [path, item] of [[["defaults"], raw.defaults], ...Object.entries(raw.instances ?? {}).map(([name, value]) => [["instances", name], value])]) {
12431
+ if (!item?.dashboard || !Object.hasOwn(item.dashboard, "office"))
12432
+ continue;
12433
+ patches.push({ path: [...path, "dashboard", "office"], remove: true });
12434
+ if (!Object.hasOwn(item, "office_access") && typeof item.dashboard.office === "boolean")
12435
+ patches.push({ path: [...path, "office_access"], value: item.dashboard.office });
12436
+ }
12437
+ this.saveFleetConfig(patches);
12438
+ this.logger.info("Stored Office access outside dashboard for downgrade compatibility");
12439
+ }
12440
+ catch (err) {
12441
+ this.logger.warn({ err }, "Could not migrate Office access; keeping the original file");
12442
+ }
12443
+ }
12185
12444
  slimFleetConfigAtStartup() {
12445
+ this.migrateOfficeStorage();
12446
+ this.migrateNoticeDeliveryStorage();
12186
12447
  const redundantPaths = collectRedundantInstanceDefaultPaths(this.rawFleetConfig);
12187
12448
  if (redundantPaths.length === 0)
12188
12449
  return;
@@ -12405,6 +12666,7 @@ export class FleetManager {
12405
12666
  this.effectToolsService?.forget(name);
12406
12667
  this.mods.forgetInstance(name);
12407
12668
  this.agentMods.forget(name);
12669
+ this.officeActsInstance?.forget(name);
12408
12670
  }
12409
12671
  catch (err) {
12410
12672
  this.logger.warn({ err: err.message, name }, "Dropping a removed instance's web theme failed");
@@ -14211,6 +14473,8 @@ export class FleetManager {
14211
14473
  return true;
14212
14474
  if (await this.handleSettingsChangeCallback(data, adapterId, adapter))
14213
14475
  return true;
14476
+ if (await this.handleKiroMigrateCallback(data, adapterId, adapter))
14477
+ return true;
14214
14478
  if (await this.handleDashboardCallback(data, adapterId, adapter))
14215
14479
  return true;
14216
14480
  if (await this.handleClassicApproval(data, adapterId, adapter))
@@ -14331,6 +14595,7 @@ export class FleetManager {
14331
14595
  const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
14332
14596
  name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
14333
14597
  claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
14598
+ kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
14334
14599
  }));
14335
14600
  const taken = new Set(fleet.map(i => i.name));
14336
14601
  const classic = (this.classicChannels?.getAll() ?? []).filter(ch => !taken.has(ch.instanceName)).map(ch => ({
@@ -14340,6 +14605,7 @@ export class FleetManager {
14340
14605
  ledgerPath: join(this.getInstanceDir(ch.instanceName), "cache-ledger.json"),
14341
14606
  claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
14342
14607
  profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
14608
+ kiroDbPath: this.kiroDbPathOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
14343
14609
  }));
14344
14610
  return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
14345
14611
  }
@@ -14360,8 +14626,19 @@ export class FleetManager {
14360
14626
  return this.credentialProfileOf(name, "claude-code") ?? "default";
14361
14627
  if (backend === "codex")
14362
14628
  return "unrecorded";
14629
+ if (backend === "kiro-cli")
14630
+ return this.credentialProfileOf(name, "kiro-cli") ?? "default";
14631
+ if (backend === "grok" || backend === "muse" || backend === "opencode" || backend === "antigravity")
14632
+ return "default";
14363
14633
  return undefined;
14364
14634
  }
14635
+ /** #2122 §10: a Kiro instance with a credential profile writes its classic store in that profile's home. */
14636
+ kiroDbPathOf(name, backend) {
14637
+ if (backend !== "kiro-cli")
14638
+ return undefined;
14639
+ const profile = this.credentialProfileOf(name, "kiro-cli");
14640
+ return profile ? join(credentialProfileStoreHome(getAgendHome(), "kiro-cli", profile), "data.sqlite3") : undefined;
14641
+ }
14365
14642
  /** #2122: the fleet's day boundary zone — defaults.scheduler.default_timezone, else this host's. */
14366
14643
  usageZone() {
14367
14644
  return this.fleetConfig?.defaults?.scheduler?.default_timezone?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
@@ -14404,6 +14681,19 @@ export class FleetManager {
14404
14681
  rolloutsUnreadable: root => sharedRolloutIndex(root).unreadable,
14405
14682
  metaPath: join(this.dataDir, "cache-codex-rollouts.json"),
14406
14683
  zone: () => this.usageZone(),
14684
+ // #2122 §10: Kiro's shared classic store (XDG_DATA_HOME, as kiro-cli resolves it) and its KIRO_HOME.
14685
+ kiroDbPath: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "kiro-cli", "data.sqlite3"),
14686
+ kiroHome: () => kiroHome(),
14687
+ // #2122 §10 step 3: grok keeps its sessions under ~/.grok (grok.ts reads them there too).
14688
+ grokSessionsDir: () => join(homedir(), ".grok", "sessions"),
14689
+ // #2122 §10 step 4: muse keeps sessions under $XDG_DATA_HOME/muse (else ~/.local/share/muse), as muse.ts reads them.
14690
+ // #2122 §10 step 5: OpenCode's database, under XDG_DATA_HOME as opencode resolves it.
14691
+ // #2122 §10 step 6: agy's conversations, under ~/.gemini as the antigravity backend reads its settings.
14692
+ antigravityDir: () => join(homedir(), ".gemini", "antigravity-cli", "conversations"),
14693
+ // The version AgEnD's CLI probe last recorded for agy: its cache file, whatever its age (never runs agy).
14694
+ antigravityVersion: () => this.lastRecordedCliVersion("antigravity"),
14695
+ opencodeDbPath: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "opencode", "opencode.db"),
14696
+ museSessionsDir: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "muse", "sessions"),
14407
14697
  log: (level, msg, extra) => this.logger[level](extra ?? {}, msg),
14408
14698
  });
14409
14699
  }
@@ -14524,6 +14814,7 @@ export class FleetManager {
14524
14814
  return this.deliveryOutbox.acknowledge(deliveryId, by);
14525
14815
  },
14526
14816
  tasks: () => this.needsYouTaskItems(),
14817
+ backendSignIns: () => this.needsYouBackendSignIns(),
14527
14818
  taskAction: action => this.taskActionFromChat(action),
14528
14819
  settings: () => ({
14529
14820
  liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
@@ -14782,14 +15073,14 @@ export class FleetManager {
14782
15073
  }
14783
15074
  }
14784
15075
  /** Post one fresh nonce-armed tip in a known General channel. */
14785
- async promptTip(generalName, adapter, chatId, threadId) {
15076
+ async promptTip(generalName, adapter, chatId, threadId, directUserId) {
14786
15077
  const state = this.readTipState();
14787
15078
  if (!state)
14788
15079
  return "unavailable";
14789
15080
  if (!state.advancedUnlocked
14790
15081
  && visibleTipLevels(true).has("advanced")
14791
15082
  && canUnlockAdvancedTips(state.dismissed)) {
14792
- return await this.promptAdvancedTipUnlock(generalName, adapter, chatId, threadId)
15083
+ return await this.promptAdvancedTipUnlock(generalName, adapter, chatId, threadId, directUserId)
14793
15084
  ? "posted"
14794
15085
  : "unavailable";
14795
15086
  }
@@ -14805,6 +15096,7 @@ export class FleetManager {
14805
15096
  chatId,
14806
15097
  threadId,
14807
15098
  message: this.formatTip(tip),
15099
+ ...(directUserId ? { deliver: (choices) => withinBudget(adapter.sendDirect(directUserId, this.formatTip(tip), { choices, disablePreview: true }), performance.now() + 5_000) } : {}),
14808
15100
  choices: [
14809
15101
  { action: "dismiss", label: t("tips.dismiss") },
14810
15102
  { action: "confused", label: t("tips.confused") },
@@ -14816,7 +15108,7 @@ export class FleetManager {
14816
15108
  });
14817
15109
  return nonce ? "posted" : "unavailable";
14818
15110
  }
14819
- async promptAdvancedTipUnlock(generalName, adapter, chatId, threadId) {
15111
+ async promptAdvancedTipUnlock(generalName, adapter, chatId, threadId, directUserId) {
14820
15112
  const nonce = await this.postNonceButtonPrompt({
14821
15113
  prefix: TIP_UNLOCK_CALLBACK_PREFIX,
14822
15114
  alertType: "tip",
@@ -14826,6 +15118,7 @@ export class FleetManager {
14826
15118
  chatId,
14827
15119
  threadId,
14828
15120
  message: t("tips.advanced.unlock_prompt"),
15121
+ ...(directUserId ? { deliver: (choices) => withinBudget(adapter.sendDirect(directUserId, t("tips.advanced.unlock_prompt"), { choices, disablePreview: true }), performance.now() + 5_000) } : {}),
14829
15122
  choices: [{ action: "unlock", label: t("tips.advanced.unlock") }],
14830
15123
  expiredText: t("tips.advanced.expired"),
14831
15124
  // No allowAnyUser: unlocking changes a persistent setting, which typed and slash `/tips advanced on` reserve for a
@@ -14845,6 +15138,15 @@ export class FleetManager {
14845
15138
  const topicId = this.fleetConfig?.instances[generalName]?.topic_id;
14846
15139
  if (!adapter || !chatId)
14847
15140
  return;
15141
+ if (this.fleetConfig && tipNoticeMode(this.fleetConfig) === "dm" && adapter.sendDirect) {
15142
+ const userId = this.noticeAdmin(adapter.id);
15143
+ if (userId) {
15144
+ const result = await this.promptTip(generalName, adapter, userId, undefined, userId);
15145
+ if (result !== "unavailable")
15146
+ return;
15147
+ // The failed prompt disarmed its nonce; a new topic prompt is safe.
15148
+ }
15149
+ }
14848
15150
  const result = await this.promptTip(generalName, adapter, chatId, topicId != null ? String(topicId) : undefined);
14849
15151
  if (result === "unavailable") {
14850
15152
  this.logger.warn({ generalName }, "Daily tip could not be posted");
@@ -16528,6 +16830,55 @@ export class FleetManager {
16528
16830
  }
16529
16831
  return this.loginController;
16530
16832
  }
16833
+ browserBackendSignInsInstance = null;
16834
+ structuredLoginCleanupFailed = false;
16835
+ pendingBackendSince = new Map();
16836
+ browserSignInOwnerCurrent(owner) {
16837
+ const record = owner;
16838
+ return !this.shuttingDown && typeof record.handle === "string" && this.webSessions?.findByHandle(record.handle) === owner
16839
+ && this.isLocalAdminSession(record.handle);
16840
+ }
16841
+ /** #2599: default-account controls share the exact writer reservation with chat/profile/install. */
16842
+ get browserBackendSignIns() {
16843
+ return this.browserBackendSignInsInstance ??= new BrowserBackendSignIns({
16844
+ dataDir: this.dataDir, lock: this.loginWindow,
16845
+ current: owner => this.browserSignInOwnerCurrent(owner),
16846
+ installed: async () => ONBOARDING_BACKENDS.filter(backend => this.isCliInstalled(backend)),
16847
+ checkAuth: backend => checkAuthStatus(LOGIN_FLOWS[backend].authCheck),
16848
+ checkConsoleAuth: () => checkAuthStatus(LOGIN_FLOWS.antigravity.authCheck),
16849
+ transport: () => createStructuredSignInTransport(),
16850
+ changed: (event, owner, launched) => {
16851
+ try {
16852
+ this.eventLog?.insert("login", "browser_backend_signin", event);
16853
+ }
16854
+ catch { /* safe metadata only */ }
16855
+ broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed"));
16856
+ this.needsYou?.poke();
16857
+ if (!this.browserSignInOwnerCurrent(owner))
16858
+ return;
16859
+ if (event.phase === "starting") {
16860
+ const record = owner;
16861
+ this.notifyFleetError(t("login.web_dashboard_notice", event.backend, record.label, record.handle.slice(0, 8)));
16862
+ }
16863
+ if (event.phase === "done") {
16864
+ void this.backendAvailability.refresh([event.backend], { force: true }).catch(() => { });
16865
+ if (launched)
16866
+ void this.recoverBackendInstances(event.backend, undefined, null).catch(() => this.logger.warn({ backend: event.backend }, "Post sign-in recovery failed"));
16867
+ }
16868
+ },
16869
+ });
16870
+ }
16871
+ needsYouBackendSignIns() {
16872
+ const pending = readPendingBackendSignIns(this.dataDir).pending;
16873
+ for (const backend of this.pendingBackendSince?.keys() ?? [])
16874
+ if (!pending.includes(backend))
16875
+ this.pendingBackendSince.delete(backend);
16876
+ return pending.map(backend => {
16877
+ if (!this.pendingBackendSince.has(backend))
16878
+ this.pendingBackendSince.set(backend, Date.now());
16879
+ return { backend, since: this.pendingBackendSince.get(backend), browser: structuredSignInSupported(backend) };
16880
+ });
16881
+ }
16531
16882
  /** #2233: sign-ins started from the web dashboard (local admin sessions), on the same controller as chat's. */
16532
16883
  webBackendLoginsInstance = null;
16533
16884
  get webBackendLogins() {
@@ -16880,6 +17231,14 @@ export class FleetManager {
16880
17231
  // "cancelled" is the detail both legacy onDone handlers keep quiet about —
16881
17232
  // a stopping fleet must not announce "login failed — fleet shutdown".
16882
17233
  await Promise.all([
17234
+ bounded(this.browserBackendSignInsInstance?.close().then(confirmed => {
17235
+ if (confirmed)
17236
+ this.browserBackendSignInsInstance = null;
17237
+ else {
17238
+ this.structuredLoginCleanupFailed = true;
17239
+ this.logger.error("Structured login cleanup failed; sign-in admission remains closed until the host is restarted.");
17240
+ }
17241
+ }), "browser-controls"),
16883
17242
  bounded(this.loginController?.shutdown(), "web-login"),
16884
17243
  bounded(this.activeInstall?.session.cancel("cancelled"), "install"),
16885
17244
  ]);
@@ -16896,6 +17255,13 @@ export class FleetManager {
16896
17255
  this.installHandoff = null;
16897
17256
  return t("login.cancelled", backend);
16898
17257
  }
17258
+ const browserLogin = this.browserBackendSignInsInstance, browserBackend = browserLogin?.activeBackend;
17259
+ if (browserLogin && browserBackend) {
17260
+ const confirmed = await browserLogin.close();
17261
+ if (confirmed)
17262
+ this.browserBackendSignInsInstance = null;
17263
+ return confirmed ? t("login.cancelled", browserBackend) : t("login.browser_cleanup_failed");
17264
+ }
16899
17265
  if (this.loginController?.isActive())
16900
17266
  return this.loginController.cancel();
16901
17267
  return t("login.no_session");
@@ -17067,7 +17433,7 @@ export class FleetManager {
17067
17433
  // session only judges that verdict; it does not read the output for URLs or prompts.
17068
17434
  const flow = {
17069
17435
  backend,
17070
- command: info.install,
17436
+ command: info.unattended ?? info.install,
17071
17437
  successPattern: /$^/,
17072
17438
  timeoutMs: 10 * 60 * 1000,
17073
17439
  };
@@ -18068,8 +18434,11 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
18068
18434
  // its ping in the reference, not the text). One hop only: a reply to a message of ours that itself answered a bot
18069
18435
  // (or the sender) is a chain of bots answering each other — a mention still carries it on. A reply to our answer
18070
18436
  // to a person is the ordinary handoff, and is addressed.
18071
- const repliesToUs = !!adapterBotUserId && msg.replyToAuthorId === adapterBotUserId && msg.replyToChain !== true;
18072
- const isMentioned = (mentionTag && text.includes(mentionTag)) || repliesToUs;
18437
+ const replyTriggers = this.classicChannels?.getReplyTriggers(channelId, msg.adapterId, this.fleetConfig?.defaults?.reply_triggers) ?? DEFAULT_REPLY_TRIGGERS;
18438
+ const repliesToUs = replyTriggers && !!adapterBotUserId && msg.replyToAuthorId === adapterBotUserId && msg.replyToChain !== true;
18439
+ // Snapshot/quoted content is context, never the forwarder's own mention.
18440
+ const addressText = msg.source === "discord" ? (msg.humanOriginalBody ?? text) : text;
18441
+ const isMentioned = (mentionTag && addressText.includes(mentionTag)) || repliesToUs;
18073
18442
  if (!isMentioned) {
18074
18443
  // Bare attachment (no @mention) — already saved above; just acknowledge.
18075
18444
  if (saved) {
@@ -18404,6 +18773,19 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
18404
18773
  catch { /* missing / stale / corrupt */ }
18405
18774
  return null;
18406
18775
  }
18776
+ /**
18777
+ * #2122 §10: the version the CLI probe last recorded for a backend — its cache file read whatever its age (the usage
18778
+ * reader's version gate needs the last known version, not a fresh one); null when none was recorded. Never a probe.
18779
+ */
18780
+ lastRecordedCliVersion(backend) {
18781
+ try {
18782
+ const v = JSON.parse(readFileSync(this.cliEnvPath(backend), "utf-8")).version;
18783
+ return typeof v === "string" && v.trim() ? v.trim() : null;
18784
+ }
18785
+ catch {
18786
+ return null;
18787
+ }
18788
+ }
18407
18789
  /** Return cached CLI versions immediately. Refresh is started after /sysinfo is sent. */
18408
18790
  getBackendCliVersionSnapshot() {
18409
18791
  const snapshot = {};
@@ -20792,6 +21174,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20792
21174
  // mark every row done and tell the user "changes applied".
20793
21175
  return { rejected: why };
20794
21176
  }
21177
+ this.migrateOfficeStorage();
21178
+ this.migrateNoticeDeliveryStorage();
20795
21179
  if (canvasWorkerEnabled(oldConfig?.web) !== this.canvasWorkerEnabled()) {
20796
21180
  this.canvasWorkerSettingChanged();
20797
21181
  }