@prestyj/cli 5.32.0 → 5.32.1

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 (221) hide show
  1. package/README.md +3 -2
  2. package/assets/skills/task-list/SKILL.md +39 -0
  3. package/dist/app-sidecar/app-settings.js +11 -5
  4. package/dist/app-sidecar/app-settings.js.map +1 -1
  5. package/dist/app-sidecar/auth-routes.js +10 -3
  6. package/dist/app-sidecar/auth-routes.js.map +1 -1
  7. package/dist/app-sidecar/git-target.d.ts +15 -2
  8. package/dist/app-sidecar/git-target.js +24 -10
  9. package/dist/app-sidecar/git-target.js.map +1 -1
  10. package/dist/app-sidecar/goal-routes.js +8 -0
  11. package/dist/app-sidecar/goal-routes.js.map +1 -1
  12. package/dist/app-sidecar/history-routes.js +7 -0
  13. package/dist/app-sidecar/history-routes.js.map +1 -1
  14. package/dist/app-sidecar/history.d.ts +5 -0
  15. package/dist/app-sidecar/history.js.map +1 -1
  16. package/dist/app-sidecar/integration-routes.js +4 -0
  17. package/dist/app-sidecar/integration-routes.js.map +1 -1
  18. package/dist/app-sidecar/mcp-routes.js +1 -0
  19. package/dist/app-sidecar/mcp-routes.js.map +1 -1
  20. package/dist/app-sidecar/mcp-rows.d.ts +2 -0
  21. package/dist/app-sidecar/mcp-rows.js +4 -1
  22. package/dist/app-sidecar/mcp-rows.js.map +1 -1
  23. package/dist/app-sidecar/prompt-routes.js +67 -1
  24. package/dist/app-sidecar/prompt-routes.js.map +1 -1
  25. package/dist/app-sidecar/route-context.d.ts +2 -0
  26. package/dist/app-sidecar/run-control-routes.js +43 -3
  27. package/dist/app-sidecar/run-control-routes.js.map +1 -1
  28. package/dist/app-sidecar/session-routes.js +10 -0
  29. package/dist/app-sidecar/session-routes.js.map +1 -1
  30. package/dist/app-sidecar/task-routes.js +2 -0
  31. package/dist/app-sidecar/task-routes.js.map +1 -1
  32. package/dist/app-sidecar/trophy-manager.d.ts +15 -0
  33. package/dist/app-sidecar/trophy-manager.js +71 -0
  34. package/dist/app-sidecar/trophy-manager.js.map +1 -0
  35. package/dist/app-sidecar/workspace-routes.js +1 -0
  36. package/dist/app-sidecar/workspace-routes.js.map +1 -1
  37. package/dist/app-sidecar.js +157 -30
  38. package/dist/app-sidecar.js.map +1 -1
  39. package/dist/cli/auth.js +22 -1
  40. package/dist/cli/auth.js.map +1 -1
  41. package/dist/cli/mcp.js +6 -1
  42. package/dist/cli/mcp.js.map +1 -1
  43. package/dist/cli.js +13 -9
  44. package/dist/cli.js.map +1 -1
  45. package/dist/config.js +4 -2
  46. package/dist/config.js.map +1 -1
  47. package/dist/core/action-threats.d.ts +68 -0
  48. package/dist/core/action-threats.js +894 -0
  49. package/dist/core/action-threats.js.map +1 -0
  50. package/dist/core/agent-notifications.d.ts +18 -0
  51. package/dist/core/agent-notifications.js +35 -2
  52. package/dist/core/agent-notifications.js.map +1 -1
  53. package/dist/core/agent-session/verification-tracker.d.ts +21 -0
  54. package/dist/core/agent-session/verification-tracker.js +39 -1
  55. package/dist/core/agent-session/verification-tracker.js.map +1 -1
  56. package/dist/core/agent-session.d.ts +166 -2
  57. package/dist/core/agent-session.js +478 -22
  58. package/dist/core/agent-session.js.map +1 -1
  59. package/dist/core/blocked-packages-file.d.ts +33 -0
  60. package/dist/core/blocked-packages-file.js +274 -0
  61. package/dist/core/blocked-packages-file.js.map +1 -0
  62. package/dist/core/compaction/policy.d.ts +2 -2
  63. package/dist/core/compaction/policy.js +86 -43
  64. package/dist/core/compaction/policy.js.map +1 -1
  65. package/dist/core/compaction/token-estimator.d.ts +4 -1
  66. package/dist/core/compaction/token-estimator.js +5 -2
  67. package/dist/core/compaction/token-estimator.js.map +1 -1
  68. package/dist/core/event-bus.d.ts +10 -0
  69. package/dist/core/event-bus.js +2 -0
  70. package/dist/core/event-bus.js.map +1 -1
  71. package/dist/core/idle-wake.d.ts +62 -0
  72. package/dist/core/idle-wake.js +103 -0
  73. package/dist/core/idle-wake.js.map +1 -0
  74. package/dist/core/injection-detect.d.ts +2 -1
  75. package/dist/core/injection-detect.js +13 -4
  76. package/dist/core/injection-detect.js.map +1 -1
  77. package/dist/core/installed-threats.d.ts +23 -0
  78. package/dist/core/installed-threats.js +71 -0
  79. package/dist/core/installed-threats.js.map +1 -0
  80. package/dist/core/mcp/catalog-cache.d.ts +2 -0
  81. package/dist/core/mcp/catalog-cache.js +4 -0
  82. package/dist/core/mcp/catalog-cache.js.map +1 -1
  83. package/dist/core/mcp/client.d.ts +8 -0
  84. package/dist/core/mcp/client.js +37 -3
  85. package/dist/core/mcp/client.js.map +1 -1
  86. package/dist/core/mcp/deferred-catalog.d.ts +12 -1
  87. package/dist/core/mcp/deferred-catalog.js +37 -10
  88. package/dist/core/mcp/deferred-catalog.js.map +1 -1
  89. package/dist/core/mcp/index.d.ts +2 -0
  90. package/dist/core/mcp/index.js +1 -0
  91. package/dist/core/mcp/index.js.map +1 -1
  92. package/dist/core/mcp/tool-guard.d.ts +58 -0
  93. package/dist/core/mcp/tool-guard.js +215 -0
  94. package/dist/core/mcp/tool-guard.js.map +1 -0
  95. package/dist/core/network-guard.d.ts +0 -20
  96. package/dist/core/network-guard.js +6 -0
  97. package/dist/core/network-guard.js.map +1 -1
  98. package/dist/core/package-cooldown.d.ts +71 -0
  99. package/dist/core/package-cooldown.js +301 -0
  100. package/dist/core/package-cooldown.js.map +1 -0
  101. package/dist/core/package-threats.d.ts +77 -3
  102. package/dist/core/package-threats.js +1250 -78
  103. package/dist/core/package-threats.js.map +1 -1
  104. package/dist/core/process-manager.d.ts +54 -12
  105. package/dist/core/process-manager.js +233 -56
  106. package/dist/core/process-manager.js.map +1 -1
  107. package/dist/core/prompt-commands.js +1 -0
  108. package/dist/core/prompt-commands.js.map +1 -1
  109. package/dist/core/session-manager.d.ts +3 -1
  110. package/dist/core/session-manager.js +2 -1
  111. package/dist/core/session-manager.js.map +1 -1
  112. package/dist/core/settings-manager.d.ts +1 -0
  113. package/dist/core/settings-manager.js +4 -0
  114. package/dist/core/settings-manager.js.map +1 -1
  115. package/dist/core/shell-threats.d.ts +14 -1
  116. package/dist/core/shell-threats.js +73 -24
  117. package/dist/core/shell-threats.js.map +1 -1
  118. package/dist/core/skill-scan.d.ts +112 -0
  119. package/dist/core/skill-scan.js +781 -0
  120. package/dist/core/skill-scan.js.map +1 -0
  121. package/dist/core/skills.d.ts +33 -0
  122. package/dist/core/skills.js +87 -16
  123. package/dist/core/skills.js.map +1 -1
  124. package/dist/core/slash-commands.d.ts +5 -1
  125. package/dist/core/slash-commands.js +24 -9
  126. package/dist/core/slash-commands.js.map +1 -1
  127. package/dist/core/steering.d.ts +7 -0
  128. package/dist/core/steering.js +13 -0
  129. package/dist/core/steering.js.map +1 -1
  130. package/dist/core/style-packs/index.js +1 -1
  131. package/dist/core/style-packs/index.js.map +1 -1
  132. package/dist/core/style-packs/packs.js +2 -2
  133. package/dist/core/subagent-manager.js +5 -4
  134. package/dist/core/subagent-manager.js.map +1 -1
  135. package/dist/core/threat-feed.d.ts +65 -0
  136. package/dist/core/threat-feed.js +329 -0
  137. package/dist/core/threat-feed.js.map +1 -0
  138. package/dist/core/trophies/catalog-more.d.ts +2 -0
  139. package/dist/core/trophies/catalog-more.js +857 -0
  140. package/dist/core/trophies/catalog-more.js.map +1 -0
  141. package/dist/core/trophies/catalog.d.ts +4 -0
  142. package/dist/core/trophies/catalog.js +649 -0
  143. package/dist/core/trophies/catalog.js.map +1 -0
  144. package/dist/core/trophies/engine.d.ts +70 -0
  145. package/dist/core/trophies/engine.js +351 -0
  146. package/dist/core/trophies/engine.js.map +1 -0
  147. package/dist/core/trophies/store.d.ts +17 -0
  148. package/dist/core/trophies/store.js +113 -0
  149. package/dist/core/trophies/store.js.map +1 -0
  150. package/dist/core/trophies/types.d.ts +119 -0
  151. package/dist/core/trophies/types.js +78 -0
  152. package/dist/core/trophies/types.js.map +1 -0
  153. package/dist/core/verification-evidence.d.ts +10 -6
  154. package/dist/core/verification-evidence.js +190 -11
  155. package/dist/core/verification-evidence.js.map +1 -1
  156. package/dist/goals-agent/goals-agent.js +3 -2
  157. package/dist/goals-agent/goals-agent.js.map +1 -1
  158. package/dist/interactive.js +16 -5
  159. package/dist/interactive.js.map +1 -1
  160. package/dist/modes/acp-mode.js +8 -1
  161. package/dist/modes/acp-mode.js.map +1 -1
  162. package/dist/modes/agent-home-mode.js +13 -1
  163. package/dist/modes/agent-home-mode.js.map +1 -1
  164. package/dist/modes/rpc-mode.d.ts +2 -1
  165. package/dist/modes/rpc-mode.js +15 -3
  166. package/dist/modes/rpc-mode.js.map +1 -1
  167. package/dist/modes/serve-mode.js +13 -1
  168. package/dist/modes/serve-mode.js.map +1 -1
  169. package/dist/system-prompt.d.ts +1 -0
  170. package/dist/system-prompt.js +12 -8
  171. package/dist/system-prompt.js.map +1 -1
  172. package/dist/test-support/slow-git.d.ts +1 -0
  173. package/dist/test-support/slow-git.js +21 -0
  174. package/dist/test-support/slow-git.js.map +1 -0
  175. package/dist/tools/background-watch-lint.d.ts +18 -0
  176. package/dist/tools/background-watch-lint.js +102 -0
  177. package/dist/tools/background-watch-lint.js.map +1 -0
  178. package/dist/tools/bash.d.ts +15 -4
  179. package/dist/tools/bash.js +72 -47
  180. package/dist/tools/bash.js.map +1 -1
  181. package/dist/tools/checklist.d.ts +3 -3
  182. package/dist/tools/compress.js +45 -0
  183. package/dist/tools/compress.js.map +1 -1
  184. package/dist/tools/edit.js +5 -0
  185. package/dist/tools/edit.js.map +1 -1
  186. package/dist/tools/generate-image.d.ts +29 -6
  187. package/dist/tools/generate-image.js +325 -151
  188. package/dist/tools/generate-image.js.map +1 -1
  189. package/dist/tools/grep.d.ts +14 -0
  190. package/dist/tools/grep.js +36 -2
  191. package/dist/tools/grep.js.map +1 -1
  192. package/dist/tools/index.d.ts +5 -1
  193. package/dist/tools/index.js +7 -4
  194. package/dist/tools/index.js.map +1 -1
  195. package/dist/tools/skill.d.ts +2 -2
  196. package/dist/tools/skill.js +8 -1
  197. package/dist/tools/skill.js.map +1 -1
  198. package/dist/tools/subagent-shared.js +4 -1
  199. package/dist/tools/subagent-shared.js.map +1 -1
  200. package/dist/tools/task-output.js +11 -1
  201. package/dist/tools/task-output.js.map +1 -1
  202. package/dist/tools/write.js +8 -0
  203. package/dist/tools/write.js.map +1 -1
  204. package/dist/ui/components/ModelSelector.d.ts +10 -0
  205. package/dist/ui/components/ModelSelector.js +51 -5
  206. package/dist/ui/components/ModelSelector.js.map +1 -1
  207. package/dist/ui/components/ToolExecution.js +2 -2
  208. package/dist/ui/components/ToolExecution.js.map +1 -1
  209. package/dist/ui/mcp.d.ts +2 -0
  210. package/dist/ui/mcp.js.map +1 -1
  211. package/dist/ui/terminal-history.js +2 -1
  212. package/dist/ui/terminal-history.js.map +1 -1
  213. package/dist/ui/tool-line-summary.js +3 -1
  214. package/dist/ui/tool-line-summary.js.map +1 -1
  215. package/dist/utils/git.d.ts +12 -3
  216. package/dist/utils/git.js +21 -8
  217. package/dist/utils/git.js.map +1 -1
  218. package/dist/utils/image.d.ts +14 -0
  219. package/dist/utils/image.js +23 -0
  220. package/dist/utils/image.js.map +1 -1
  221. package/package.json +4 -4
@@ -11,11 +11,12 @@
11
11
  * The agent spine (@prestyj/ai → @prestyj/agent → @prestyj/core) and every tool are reused
12
12
  * unchanged via AgentSession — this file is only a network seam.
13
13
  */
14
+ import { startThreatFeedSync } from "./core/threat-feed.js";
14
15
  import http from "node:http";
15
16
  import fs from "node:fs/promises";
16
17
  import os from "node:os";
17
18
  import path from "node:path";
18
- import { randomUUID } from "node:crypto";
19
+ import { createHash, randomUUID } from "node:crypto";
19
20
  import { environmentSecrets, formatChatError, redactValue } from "@prestyj/ai";
20
21
  import { createAppErrorPayload } from "./app-error.js";
21
22
  import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
@@ -23,13 +24,14 @@ import { setStreamDiagnostic } from "@prestyj/agent";
23
24
  import { AgentSession } from "./core/agent-session.js";
24
25
  import { RunLifecycle } from "./core/run-lifecycle.js";
25
26
  import { RunClaim } from "./core/run-claim.js";
27
+ import { IdleWakeController } from "./core/idle-wake.js";
26
28
  import { IMPLEMENT_PLAN_PROMPT as SHARED_IMPLEMENT_PLAN_PROMPT, PLAN_SUBMITTED_FOR_REVIEW, shouldOfferPendingPlan, } from "./core/plan-handoff.js";
27
29
  import { createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
28
30
  import { createMotionAgentSession } from "./motion-agent/motion-agent.js";
29
31
  import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
30
32
  import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
31
33
  import { buildNolanSystemPrompt, buildNolanAutopilotSystemPrompt } from "./core/nolan-prompt.js";
32
- import { countAssistantMessages } from "./core/autopilot-gate.js";
34
+ import { countAssistantMessages, extractTurnToolCalls, } from "./core/autopilot-gate.js";
33
35
  import { describeRunVerification, describeTurnVerification } from "./core/run-status.js";
34
36
  import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
35
37
  import { AuthStorage } from "./core/auth-storage.js";
@@ -39,7 +41,7 @@ import { listAllEndpoints, syncEndpointCredentials } from "./core/local-endpoint
39
41
  import { AUTH_PROVIDERS, authPriorityNote, describeAuthMethods, } from "./core/auth-providers.js";
40
42
  import { ensureAppDirs, loadSavedSettings } from "./config.js";
41
43
  import { SettingsManager } from "./core/settings-manager.js";
42
- import { getModel, getDefaultThinkingLevel, getContextWindow, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
44
+ import { getModel, withoutPaidTier, getDefaultThinkingLevel, getContextWindow, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
43
45
  import { resolveStartOrFallback } from "./core/resolve-start.js";
44
46
  import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
45
47
  import { scoreProjectHealth } from "./core/project-health-score.js";
@@ -62,6 +64,8 @@ import { parseWorkspaceMode, } from "./app-sidecar/session-types.js";
62
64
  import { ALL_PROVIDERS, loadProjectModelPrefs, loadNolanModelPref, loadAutopilot, } from "./app-sidecar/app-settings.js";
63
65
  import { NOLAN_ALLOWED_TOOLS } from "./app-sidecar/nolan-context.js";
64
66
  import { createProgressManager } from "./app-sidecar/progress-manager.js";
67
+ import { createTrophyManager } from "./app-sidecar/trophy-manager.js";
68
+ import { localTrophyDate } from "./core/trophies/engine.js";
65
69
  import { handleSessionRoutes } from "./app-sidecar/session-routes.js";
66
70
  import { handleGoalRoutes } from "./app-sidecar/goal-routes.js";
67
71
  import { readGitState, resolveGitCwd } from "./app-sidecar/git-target.js";
@@ -78,6 +82,8 @@ import { handleMcpRoutes } from "./app-sidecar/mcp-routes.js";
78
82
  import { createHfPullManager } from "./app-sidecar/hf-pull-manager.js";
79
83
  import { keepAwake } from "./app-sidecar/keep-awake.js";
80
84
  import { createAutopilotRunner } from "./app-sidecar/autopilot-runner.js";
85
+ /** Limit for re-reading git state in the background when git missed the usual 2 s. */
86
+ const SLOW_GIT_READ_TIMEOUT_MS = 15_000;
81
87
  async function main() {
82
88
  // Hidden persistent-worker dispatch must win before strict JSON/server parsing.
83
89
  if (process.argv.includes("--subagent-worker")) {
@@ -161,6 +167,9 @@ async function main() {
161
167
  // Sweep recoverable full tool outputs (~/.ezcoder/tool-output/) older than 48h.
162
168
  // Fire-and-forget: cleanup must never delay or break startup.
163
169
  void cleanupToolOutputs().catch(() => { });
170
+ // Daily malicious-package feed (OSV MAL-, npm + PyPI): deferred, background,
171
+ // fail-silent; at most one sync a day (core/threat-feed.ts).
172
+ startThreatFeedSync();
164
173
  const auth = new AuthStorage(paths.authFile);
165
174
  await auth.load();
166
175
  // Every window's session lives here as an in-process object, keyed by the id
@@ -215,6 +224,20 @@ async function main() {
215
224
  for (const ctx of sessions.values())
216
225
  ctx.broadcast("progress", { ...snapshot, origin: ctx.id === originId });
217
226
  });
227
+ // Trophies — daemon-level like progress. XP totals (commits, streak, level)
228
+ // come from the progress snapshot rather than being counted twice.
229
+ const trophies = await createTrophyManager(() => {
230
+ const p = progress.snapshot();
231
+ return {
232
+ prompts: p.totals.prompts,
233
+ commits: p.totals.commits,
234
+ linesShipped: p.totals.linesShipped,
235
+ level: p.level,
236
+ };
237
+ }, (snapshot, originId) => {
238
+ for (const ctx of sessions.values())
239
+ ctx.broadcast("trophies", { ...snapshot, origin: ctx.id === originId });
240
+ });
218
241
  const { subscriptionUsage } = createUsageService(auth);
219
242
  // git status / GitHub counts / CI pollers, one per repo however many windows
220
243
  // have it open (each window used to run its own).
@@ -280,6 +303,7 @@ async function main() {
280
303
  auth,
281
304
  paths,
282
305
  progress,
306
+ trophies,
283
307
  memoryStore,
284
308
  jiwaStore,
285
309
  broadcastAll,
@@ -363,6 +387,11 @@ async function main() {
363
387
  daemonJson(res, 200, progress.snapshot());
364
388
  return;
365
389
  }
390
+ // Trophies are app-wide too: earned anywhere, shown in every window.
391
+ if (method === "GET" && url === "/trophies") {
392
+ daemonJson(res, 200, trophies.snapshot());
393
+ return;
394
+ }
366
395
  // Subscription quota is account-wide, not project/session-specific. OAuth
367
396
  // tokens stay in this daemon; only the active provider's normalized snapshot
368
397
  // reaches the webview.
@@ -455,7 +484,7 @@ async function main() {
455
484
  * logger, PATH, shared auth file, and radio live at the daemon level.
456
485
  */
457
486
  async function createSession(deps, opts) {
458
- const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders, repoPolls, sessionIdsAt, busyAt, } = deps;
487
+ const { auth, progress, trophies, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders, repoPolls, sessionIdsAt, busyAt, } = deps;
459
488
  const paths = deps.paths;
460
489
  const mode = opts.mode;
461
490
  let chatAgent = opts.chatAgent;
@@ -643,6 +672,7 @@ async function createSession(deps, opts) {
643
672
  if (started) {
644
673
  goal = { dir: current.dir, record: started };
645
674
  broadcast("goal_state", { id: started.id, phase: goalPhase(started) });
675
+ void trophies.count("goalsStarted", opts.id);
646
676
  }
647
677
  };
648
678
  const askUserTool = createAskUserTool(async (request) => {
@@ -887,7 +917,8 @@ async function createSession(deps, opts) {
887
917
  // follows the active model. A Goal window reads git from its attached
888
918
  // project folder, not the goal folder (see resolveGitCwd), and re-targets
889
919
  // when the goal's folders change (`retargetGit`).
890
- let gitCwd = await resolveGitCwd(cwd, goal?.record ?? null);
920
+ const initialTarget = await resolveGitCwd(cwd, goal?.record ?? null);
921
+ let gitCwd = initialTarget.dir;
891
922
  const initialGit = await readGitState(gitCwd);
892
923
  let gitBranch = initialGit.branch;
893
924
  let gitIsRepo = initialGit.isRepo;
@@ -1114,13 +1145,19 @@ async function createSession(deps, opts) {
1114
1145
  session.eventBus.on("model_change", (d) => broadcast("model_change", d));
1115
1146
  session.eventBus.on("hook", (d) => broadcast("hook", d));
1116
1147
  session.eventBus.on("diagnostics", (d) => broadcast("diagnostics", d));
1148
+ session.eventBus.on("security_notice", (d) => broadcast("security_notice", d));
1149
+ session.eventBus.on("security_block", (d) => broadcast("security_block", d));
1117
1150
  // Fires BEFORE the candidate final answer streams. The webview holds assistant
1118
1151
  // text back while armed, so an injected follow-up supersedes a draft that was never
1119
1152
  // painted instead of deleting one the user already started reading.
1120
1153
  session.eventBus.on("hook_armed", (d) => broadcast("hook_armed", d));
1121
1154
  session.eventBus.on("subagent_state", (d) => broadcast("subagent_state", d));
1122
1155
  session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
1123
- session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
1156
+ session.eventBus.on("compaction_end", (d) => {
1157
+ broadcast("compaction_end", d);
1158
+ if (d.compacted)
1159
+ void trophies.count("compactions", opts.id);
1160
+ });
1124
1161
  // Cold-prompt-cache notice: push the fresh TTL anchor + context size whenever
1125
1162
  // a run or compaction settles. Expiry itself is time-based, so the webview
1126
1163
  // also re-reads `cacheExpiry` from /state when the user returns or types.
@@ -1184,6 +1221,9 @@ async function createSession(deps, opts) {
1184
1221
  // Bumped by /cancel — a run whose cancel generation changed mid-flight was
1185
1222
  // canceled and earns no XP.
1186
1223
  let cancelGeneration = 0;
1224
+ // Wakes an idle session for its own failed background work; set up once the
1225
+ // run machinery below exists. Null outside the coding agent.
1226
+ let idleWake = null;
1187
1227
  // Autopilot (auto-review) toggle for THIS window's project. Loaded from
1188
1228
  // ezcoder-app.json on boot; flipped via POST /autopilot. When on, POST /prompt runs
1189
1229
  // runAutopilotCycle after the user's turn settles — Nolan auto-reviews the work
@@ -1314,20 +1354,25 @@ async function createSession(deps, opts) {
1314
1354
  });
1315
1355
  nolanModelOverride = null;
1316
1356
  }
1317
- /** The model Nolan uses next turn: the pin when set, else EZ Coder's. */
1357
+ /** The model Nolan uses next turn: the pin when set, else EZ Coder's. Nolan never
1358
+ * follows GG onto a paid tier: he runs in the background, unwatched. */
1318
1359
  function nolanCurrentModel() {
1319
1360
  if (nolanModelOverride)
1320
1361
  return nolanModelOverride;
1321
1362
  const st = session.getState();
1322
- return { provider: st.provider, model: st.model };
1363
+ return { provider: st.provider, model: withoutPaidTier(st.model) };
1323
1364
  }
1324
1365
  /** Footer payload: Nolan's effective model + whether it's a pin. Merged into
1325
1366
  * /state, the SSE ready frame, and every nolan_model_change broadcast. */
1326
1367
  function nolanStatePayload() {
1327
1368
  const st = session.getState();
1328
- return effectiveNolanModel(nolanModelOverride, { provider: st.provider, model: st.model });
1369
+ return effectiveNolanModel(nolanModelOverride, {
1370
+ provider: st.provider,
1371
+ model: withoutPaidTier(st.model),
1372
+ });
1329
1373
  }
1330
- async function syncNolanModel(provider, model) {
1374
+ async function syncNolanModel(provider, requested) {
1375
+ const model = withoutPaidTier(requested);
1331
1376
  if (nolanRunning) {
1332
1377
  pendingNolanModel = { provider, model };
1333
1378
  return;
@@ -1398,7 +1443,9 @@ async function createSession(deps, opts) {
1398
1443
  let nolanAutoSession = null;
1399
1444
  let nolanAutoAbort = new AbortController();
1400
1445
  let pendingNolanAutoModel = null;
1401
- async function syncNolanAutoModel(provider, model) {
1446
+ async function syncNolanAutoModel(provider, requested) {
1447
+ // The autopilot reviewer runs every turn, unwatched: never on a paid tier.
1448
+ const model = withoutPaidTier(requested);
1402
1449
  if (autopilotReviewing) {
1403
1450
  pendingNolanAutoModel = { provider, model };
1404
1451
  return;
@@ -1443,6 +1490,8 @@ async function createSession(deps, opts) {
1443
1490
  }
1444
1491
  function abortOwnedWork() {
1445
1492
  cancelGeneration++;
1493
+ // Stop means "stop acting on your own" — that includes background alerts.
1494
+ idleWake?.noteStop();
1446
1495
  abort.abort();
1447
1496
  // An MCP tool call parked on user input is not cancelled by the signal —
1448
1497
  // the promise lives in the bridge. Release it, or the aborted turn's tool
@@ -1487,6 +1536,7 @@ async function createSession(deps, opts) {
1487
1536
  const runStartedAt = Date.now();
1488
1537
  const cancelGenAtStart = cancelGeneration;
1489
1538
  const assistantsBeforeRun = countAssistantMessages(session.getMessages());
1539
+ const messagesBeforeRun = session.getMessages().length;
1490
1540
  let runSucceeded = false;
1491
1541
  broadcast("run_start", {
1492
1542
  text: label,
@@ -1521,7 +1571,25 @@ async function createSession(deps, opts) {
1521
1571
  cancelGeneration === cancelGenAtStart &&
1522
1572
  countAssistantMessages(session.getMessages()) > assistantsBeforeRun) {
1523
1573
  // Fire-and-forget — XP must never delay or break run teardown.
1524
- void progress.awardRun(cwd, runStartedAt, opts.id);
1574
+ // Trophies follow the award so they see this run's commits and level.
1575
+ const st = session.getState();
1576
+ const runFacts = {
1577
+ mode,
1578
+ chatAgent: mode === "chat" ? chatAgent : undefined,
1579
+ goal: goal !== null,
1580
+ provider: st.provider,
1581
+ model: st.model,
1582
+ tools: extractTurnToolCalls(session.getMessages(), messagesBeforeRun),
1583
+ durationMs: Date.now() - runStartedAt,
1584
+ hour: new Date().getHours(),
1585
+ day: new Date().getDay(),
1586
+ date: localTrophyDate(new Date()),
1587
+ // A hash, so the trophies file never holds project paths.
1588
+ project: createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 16),
1589
+ };
1590
+ void progress
1591
+ .awardRun(cwd, runStartedAt, opts.id)
1592
+ .then(() => trophies.recordRun(runFacts, opts.id));
1525
1593
  }
1526
1594
  // A run may have switched branches, changed files, or spawned/finished
1527
1595
  // background tasks. Refresh the workspace extras once it settles.
@@ -1779,6 +1847,42 @@ async function createSession(deps, opts) {
1779
1847
  taskRunAll = v;
1780
1848
  },
1781
1849
  });
1850
+ // ── Idle wake for background alerts ────────────────────────
1851
+ // A background task that fails, or a child agent that finishes, after the
1852
+ // agent ended its turn would otherwise wait for the user's next message.
1853
+ // The coding agent only: chat agents start no background work, Motion owns
1854
+ // its own render flow, and GG Goals drives its workers itself.
1855
+ if (mode !== "chat" && mode !== "motion" && !goal) {
1856
+ idleWake = new IdleWakeController({
1857
+ isBusy: () => running ||
1858
+ runClaim.active ||
1859
+ autopilotActive ||
1860
+ autopilotReviewing ||
1861
+ drainingStrandedQueue ||
1862
+ taskRunAll,
1863
+ isBlocked: () => pendingPlanPath !== null,
1864
+ hasPendingAlert: () => session.hasPendingBackgroundAlert(),
1865
+ humanPromptSeq: () => session.getHumanPromptSeq(),
1866
+ onSkip: (reason) => log("INFO", "app-sidecar", "background alert left for the next message", { reason }),
1867
+ start: async () => {
1868
+ // Same race guard as POST /prompt: claim synchronously or yield.
1869
+ if (!runClaim.claim())
1870
+ return;
1871
+ try {
1872
+ log("INFO", "app-sidecar", "starting a run for a background alert");
1873
+ await runAgent("Background task update", async () => {
1874
+ await session.continueFromNotifications();
1875
+ });
1876
+ await runStrandedQueue();
1877
+ }
1878
+ finally {
1879
+ runClaim.release();
1880
+ offerPendingPlan();
1881
+ }
1882
+ },
1883
+ });
1884
+ session.onBackgroundAlert(() => idleWake?.trigger());
1885
+ }
1782
1886
  // ── Provider auth (login) bridge ───────────────────────────
1783
1887
  // OAuth login functions are interactive (open a URL, sometimes prompt for a
1784
1888
  // pasted code). We run one at a time and surface every step over SSE so the
@@ -1827,11 +1931,15 @@ async function createSession(deps, opts) {
1827
1931
  ? ((await auth.getCredentials(oauthKey))?.usageExhaustedUntil ?? 0)
1828
1932
  : 0;
1829
1933
  const oauthSidelined = hasOAuth && Date.now() < exhaustedUntil && hasApiKey;
1830
- const activeMethod = connectedMethods.includes("oauth")
1831
- ? oauthSidelined
1832
- ? "apikey"
1833
- : "oauth"
1834
- : connectedMethods[0];
1934
+ // Gemini resolves the API key first (model registry authStorageKeys).
1935
+ const keyFirst = p.value === "gemini" && hasApiKey;
1936
+ const activeMethod = keyFirst
1937
+ ? "apikey"
1938
+ : connectedMethods.includes("oauth")
1939
+ ? oauthSidelined
1940
+ ? "apikey"
1941
+ : "oauth"
1942
+ : connectedMethods[0];
1835
1943
  // `methodDetails` is the server-side lookup table behind
1836
1944
  // `methodGuidance` — shipping both would duplicate every string on the
1837
1945
  // wire for no consumer.
@@ -1928,31 +2036,48 @@ async function createSession(deps, opts) {
1928
2036
  let gitTargetGeneration = 0;
1929
2037
  /**
1930
2038
  * A goal's attached folders changed: point the titlebar's git state at the
1931
- * new project folder. No-op when the target is unchanged; a newer call or
1932
- * teardown supersedes one still reading git.
2039
+ * new project folder. No-op when the target is unchanged, unless `reread`
2040
+ * asks for a fresh read of the same folder; a newer call or teardown
2041
+ * supersedes one still reading git.
1933
2042
  */
1934
- async function retargetGit() {
2043
+ async function retargetGit(options = {}) {
1935
2044
  const generation = ++gitTargetGeneration;
1936
- const next = await resolveGitCwd(cwd, goal?.record ?? null);
1937
- if (generation !== gitTargetGeneration || repoWatchStopped || next === gitCwd)
2045
+ const read = { timeoutMs: options.timeoutMs };
2046
+ const { dir: next } = await resolveGitCwd(cwd, goal?.record ?? null, (dir) => isGitRepo(dir, read));
2047
+ if (generation !== gitTargetGeneration || repoWatchStopped)
2048
+ return;
2049
+ if (next === gitCwd && !options.reread)
1938
2050
  return;
1939
- const state = await readGitState(next);
2051
+ const state = await readGitState(next, read);
1940
2052
  if (generation !== gitTargetGeneration || repoWatchStopped)
1941
2053
  return;
1942
- repoWatch.unsubscribe();
2054
+ const moved = next !== gitCwd || state.gitHubSlug !== gitHubSlug;
1943
2055
  gitCwd = next;
1944
2056
  gitBranch = state.branch;
1945
2057
  gitIsRepo = state.isRepo;
1946
2058
  gitDirtyFileCount = state.dirtyFileCount;
1947
2059
  gitHubSlug = state.gitHubSlug;
1948
- // The old repo's numbers don't apply; the new watchers fill these in.
1949
- gitHubIssues = null;
1950
- gitHubPRs = null;
1951
- gitHubCI = null;
1952
- projectHealthScan = null;
1953
- repoWatch = watchRepo(gitCwd);
2060
+ if (moved) {
2061
+ // The old repo's numbers don't apply; the new watchers fill these in.
2062
+ repoWatch.unsubscribe();
2063
+ gitHubIssues = null;
2064
+ gitHubPRs = null;
2065
+ gitHubCI = null;
2066
+ projectHealthScan = null;
2067
+ repoWatch = watchRepo(gitCwd);
2068
+ }
1954
2069
  broadcast("extras", footerExtras());
1955
2070
  }
2071
+ // Git missed its 2 s limit at startup (a busy machine): the titlebar would
2072
+ // show "not a repo" until the next run. Read again in the background with
2073
+ // more time; window creation doesn't wait for it.
2074
+ if (!initialTarget.settled || !initialGit.settled) {
2075
+ retargetGit({ reread: true, timeoutMs: SLOW_GIT_READ_TIMEOUT_MS }).catch((err) => {
2076
+ log("WARN", "app-sidecar", "slow git re-read failed", {
2077
+ message: err instanceof Error ? err.message : String(err),
2078
+ });
2079
+ });
2080
+ }
1956
2081
  const routeCtx = {
1957
2082
  get session() {
1958
2083
  return session;
@@ -1983,6 +2108,7 @@ async function createSession(deps, opts) {
1983
2108
  nolanStatePayload,
1984
2109
  footerExtras,
1985
2110
  progress,
2111
+ trophies,
1986
2112
  memoryStore,
1987
2113
  jiwaStore,
1988
2114
  get clientSeq() {
@@ -2202,6 +2328,7 @@ async function createSession(deps, opts) {
2202
2328
  // started. Otherwise they keep streaming tokens and running tools on a
2203
2329
  // session that is being torn down.
2204
2330
  abortOwnedWork();
2331
+ idleWake?.dispose();
2205
2332
  cancelHfPull();
2206
2333
  releaseRunAwake?.();
2207
2334
  releaseRunAwake = null;