@prestyj/cli 5.30.1 → 5.30.3

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 (245) hide show
  1. package/README.md +54 -38
  2. package/dist/app-sidecar/app-settings.d.ts +6 -2
  3. package/dist/app-sidecar/app-settings.js +39 -20
  4. package/dist/app-sidecar/app-settings.js.map +1 -1
  5. package/dist/app-sidecar/auth-routes.js +4 -1
  6. package/dist/app-sidecar/auth-routes.js.map +1 -1
  7. package/dist/app-sidecar/autopilot-runner.js +2 -5
  8. package/dist/app-sidecar/autopilot-runner.js.map +1 -1
  9. package/dist/app-sidecar/git-target.d.ts +17 -0
  10. package/dist/app-sidecar/git-target.js +31 -0
  11. package/dist/app-sidecar/git-target.js.map +1 -0
  12. package/dist/app-sidecar/goal-routes.d.ts +26 -0
  13. package/dist/app-sidecar/goal-routes.js +445 -0
  14. package/dist/app-sidecar/goal-routes.js.map +1 -0
  15. package/dist/app-sidecar/history-routes.d.ts +13 -0
  16. package/dist/app-sidecar/history-routes.js +36 -25
  17. package/dist/app-sidecar/history-routes.js.map +1 -1
  18. package/dist/app-sidecar/mcp-routes.js +1 -2
  19. package/dist/app-sidecar/mcp-routes.js.map +1 -1
  20. package/dist/app-sidecar/repo-polls.d.ts +5 -2
  21. package/dist/app-sidecar/repo-polls.js +20 -0
  22. package/dist/app-sidecar/repo-polls.js.map +1 -1
  23. package/dist/app-sidecar/route-context.d.ts +14 -0
  24. package/dist/app-sidecar/run-control-routes.js +2 -1
  25. package/dist/app-sidecar/run-control-routes.js.map +1 -1
  26. package/dist/app-sidecar/session-routes.js +41 -0
  27. package/dist/app-sidecar/session-routes.js.map +1 -1
  28. package/dist/app-sidecar/session-types.d.ts +2 -0
  29. package/dist/app-sidecar/workspace-routes.js +13 -95
  30. package/dist/app-sidecar/workspace-routes.js.map +1 -1
  31. package/dist/app-sidecar.js +237 -50
  32. package/dist/app-sidecar.js.map +1 -1
  33. package/dist/chat-agents/jiwa.js +2 -0
  34. package/dist/chat-agents/jiwa.js.map +1 -1
  35. package/dist/chat-agents/memory.js +2 -0
  36. package/dist/chat-agents/memory.js.map +1 -1
  37. package/dist/cli/auth.js +13 -9
  38. package/dist/cli/auth.js.map +1 -1
  39. package/dist/cli/shared.js +1 -1
  40. package/dist/cli/shared.js.map +1 -1
  41. package/dist/cli.js +8 -9
  42. package/dist/cli.js.map +1 -1
  43. package/dist/core/acceptance-checks.js +17 -4
  44. package/dist/core/acceptance-checks.js.map +1 -1
  45. package/dist/core/agent-notifications.d.ts +1 -1
  46. package/dist/core/agent-session/verification-tracker.d.ts +5 -1
  47. package/dist/core/agent-session/verification-tracker.js +25 -6
  48. package/dist/core/agent-session/verification-tracker.js.map +1 -1
  49. package/dist/core/agent-session.d.ts +83 -5
  50. package/dist/core/agent-session.js +174 -59
  51. package/dist/core/agent-session.js.map +1 -1
  52. package/dist/core/ask-user.d.ts +11 -0
  53. package/dist/core/ask-user.js +6 -1
  54. package/dist/core/ask-user.js.map +1 -1
  55. package/dist/core/auth-providers.d.ts +2 -80
  56. package/dist/core/auth-providers.js +4 -183
  57. package/dist/core/auth-providers.js.map +1 -1
  58. package/dist/core/autopilot-cycle.d.ts +2 -0
  59. package/dist/core/autopilot-cycle.js +21 -2
  60. package/dist/core/autopilot-cycle.js.map +1 -1
  61. package/dist/core/cache-diagnostics.d.ts +1 -1
  62. package/dist/core/cache-diagnostics.js +2 -1
  63. package/dist/core/cache-diagnostics.js.map +1 -1
  64. package/dist/core/checklist-prompt.d.ts +9 -2
  65. package/dist/core/checklist-prompt.js +26 -7
  66. package/dist/core/checklist-prompt.js.map +1 -1
  67. package/dist/core/checklist-snapshot.js +1 -1
  68. package/dist/core/checklist-snapshot.js.map +1 -1
  69. package/dist/core/checklist-store.d.ts +5 -1
  70. package/dist/core/checklist-store.js +35 -4
  71. package/dist/core/checklist-store.js.map +1 -1
  72. package/dist/core/compaction/tool-result-pruner.d.ts +10 -0
  73. package/dist/core/compaction/tool-result-pruner.js +12 -0
  74. package/dist/core/compaction/tool-result-pruner.js.map +1 -1
  75. package/dist/core/context-files.d.ts +8 -0
  76. package/dist/core/context-files.js +46 -0
  77. package/dist/core/context-files.js.map +1 -0
  78. package/dist/core/env-delta.d.ts +9 -1
  79. package/dist/core/env-delta.js +20 -0
  80. package/dist/core/env-delta.js.map +1 -1
  81. package/dist/core/event-bus.d.ts +1 -1
  82. package/dist/core/event-bus.js +39 -13
  83. package/dist/core/event-bus.js.map +1 -1
  84. package/dist/core/extensions/plugin-bundles.js +4 -2
  85. package/dist/core/extensions/plugin-bundles.js.map +1 -1
  86. package/dist/core/goals-store.d.ts +208 -0
  87. package/dist/core/goals-store.js +521 -0
  88. package/dist/core/goals-store.js.map +1 -0
  89. package/dist/core/hashline.d.ts +10 -8
  90. package/dist/core/hashline.js.map +1 -1
  91. package/dist/core/local-endpoint-store.js +15 -11
  92. package/dist/core/local-endpoint-store.js.map +1 -1
  93. package/dist/core/mcp/client.js +2 -0
  94. package/dist/core/mcp/client.js.map +1 -1
  95. package/dist/core/mcp/loopback.d.ts +2 -2
  96. package/dist/core/mcp/loopback.js +2 -2
  97. package/dist/core/mcp/shared-pool.js +4 -3
  98. package/dist/core/mcp/shared-pool.js.map +1 -1
  99. package/dist/core/persistent-shell.js +2 -1
  100. package/dist/core/persistent-shell.js.map +1 -1
  101. package/dist/core/process-manager.js +9 -1
  102. package/dist/core/process-manager.js.map +1 -1
  103. package/dist/core/project-discovery.d.ts +7 -0
  104. package/dist/core/project-discovery.js +9 -3
  105. package/dist/core/project-discovery.js.map +1 -1
  106. package/dist/core/project-health-scan.d.ts +109 -0
  107. package/dist/core/project-health-scan.js +673 -0
  108. package/dist/core/project-health-scan.js.map +1 -0
  109. package/dist/core/project-health-score.d.ts +41 -0
  110. package/dist/core/project-health-score.js +257 -0
  111. package/dist/core/project-health-score.js.map +1 -0
  112. package/dist/core/prompt-commands.js +44 -6
  113. package/dist/core/prompt-commands.js.map +1 -1
  114. package/dist/core/radio.d.ts +2 -3
  115. package/dist/core/radio.js.map +1 -1
  116. package/dist/core/session-manager.d.ts +7 -1
  117. package/dist/core/session-manager.js +17 -7
  118. package/dist/core/session-manager.js.map +1 -1
  119. package/dist/core/session-storage.d.ts +4 -0
  120. package/dist/core/session-storage.js +9 -3
  121. package/dist/core/session-storage.js.map +1 -1
  122. package/dist/core/steroids.d.ts +8 -0
  123. package/dist/core/steroids.js +19 -3
  124. package/dist/core/steroids.js.map +1 -1
  125. package/dist/core/subagent-manager.d.ts +51 -0
  126. package/dist/core/subagent-manager.js +114 -22
  127. package/dist/core/subagent-manager.js.map +1 -1
  128. package/dist/core/subagent-receipt.js +3 -4
  129. package/dist/core/subagent-receipt.js.map +1 -1
  130. package/dist/core/subagent-store.js +2 -1
  131. package/dist/core/subagent-store.js.map +1 -1
  132. package/dist/core/ui-adoption.js +2 -0
  133. package/dist/core/ui-adoption.js.map +1 -1
  134. package/dist/core/ui-registry.js +5 -4
  135. package/dist/core/ui-registry.js.map +1 -1
  136. package/dist/core/verification-evidence.d.ts +13 -2
  137. package/dist/core/verification-evidence.js +256 -3
  138. package/dist/core/verification-evidence.js.map +1 -1
  139. package/dist/core/verification-gate.js +4 -2
  140. package/dist/core/verification-gate.js.map +1 -1
  141. package/dist/core/verify-commands.js +2 -3
  142. package/dist/core/verify-commands.js.map +1 -1
  143. package/dist/goals-agent/goal-team.d.ts +2 -0
  144. package/dist/goals-agent/goal-team.js +83 -0
  145. package/dist/goals-agent/goal-team.js.map +1 -0
  146. package/dist/goals-agent/goals-agent.d.ts +90 -0
  147. package/dist/goals-agent/goals-agent.js +246 -0
  148. package/dist/goals-agent/goals-agent.js.map +1 -0
  149. package/dist/index.js +1 -1
  150. package/dist/index.js.map +1 -1
  151. package/dist/interactive.js +4 -3
  152. package/dist/interactive.js.map +1 -1
  153. package/dist/modes/agent-home-mode.js +1 -4
  154. package/dist/modes/agent-home-mode.js.map +1 -1
  155. package/dist/modes/subagent-worker-mode.d.ts +14 -0
  156. package/dist/modes/subagent-worker-mode.js +45 -9
  157. package/dist/modes/subagent-worker-mode.js.map +1 -1
  158. package/dist/system-prompt.d.ts +8 -0
  159. package/dist/system-prompt.js +41 -20
  160. package/dist/system-prompt.js.map +1 -1
  161. package/dist/tools/bash-read-evidence.d.ts +13 -0
  162. package/dist/tools/bash-read-evidence.js +48 -1
  163. package/dist/tools/bash-read-evidence.js.map +1 -1
  164. package/dist/tools/bash.d.ts +3 -1
  165. package/dist/tools/bash.js +8 -4
  166. package/dist/tools/bash.js.map +1 -1
  167. package/dist/tools/checklist.d.ts +5 -4
  168. package/dist/tools/checklist.js +17 -4
  169. package/dist/tools/checklist.js.map +1 -1
  170. package/dist/tools/edit.d.ts +1 -0
  171. package/dist/tools/edit.js +84 -1
  172. package/dist/tools/edit.js.map +1 -1
  173. package/dist/tools/grep.js +3 -2
  174. package/dist/tools/grep.js.map +1 -1
  175. package/dist/tools/index.d.ts +7 -1
  176. package/dist/tools/index.js +19 -8
  177. package/dist/tools/index.js.map +1 -1
  178. package/dist/tools/read-only-bash.js +3 -0
  179. package/dist/tools/read-only-bash.js.map +1 -1
  180. package/dist/tools/read-tracker.d.ts +6 -12
  181. package/dist/tools/read-tracker.js +21 -36
  182. package/dist/tools/read-tracker.js.map +1 -1
  183. package/dist/tools/read.js +2 -2
  184. package/dist/tools/read.js.map +1 -1
  185. package/dist/tools/subagent-control.js +22 -3
  186. package/dist/tools/subagent-control.js.map +1 -1
  187. package/dist/tools/write.js +110 -8
  188. package/dist/tools/write.js.map +1 -1
  189. package/dist/ui/App.js +20 -26
  190. package/dist/ui/App.js.map +1 -1
  191. package/dist/ui/components/ActivityIndicator.d.ts +5 -5
  192. package/dist/ui/components/InputArea.d.ts +2 -2
  193. package/dist/ui/components/InputArea.js +3 -5
  194. package/dist/ui/components/InputArea.js.map +1 -1
  195. package/dist/ui/components/ToolExecution.d.ts +2 -2
  196. package/dist/ui/components/ToolExecution.js +4 -3
  197. package/dist/ui/components/ToolExecution.js.map +1 -1
  198. package/dist/ui/hooks/useAgentLoop.js +7 -6
  199. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  200. package/dist/ui/hooks/useContextCompaction.js +38 -31
  201. package/dist/ui/hooks/useContextCompaction.js.map +1 -1
  202. package/dist/ui/sessions.js +1 -5
  203. package/dist/ui/sessions.js.map +1 -1
  204. package/dist/ui/utils/highlight.d.ts +0 -5
  205. package/dist/ui/utils/highlight.js +3 -49
  206. package/dist/ui/utils/highlight.js.map +1 -1
  207. package/dist/ui/utils/markdown-renderer.d.ts +6 -0
  208. package/dist/ui/utils/markdown-renderer.js +21 -5
  209. package/dist/ui/utils/markdown-renderer.js.map +1 -1
  210. package/dist/utils/format.d.ts +0 -4
  211. package/dist/utils/format.js +0 -7
  212. package/dist/utils/format.js.map +1 -1
  213. package/dist/utils/process.js +3 -3
  214. package/dist/utils/process.js.map +1 -1
  215. package/package.json +5 -8
  216. package/dist/ui/activity-phrases.d.ts +0 -10
  217. package/dist/ui/activity-phrases.js +0 -184
  218. package/dist/ui/activity-phrases.js.map +0 -1
  219. package/dist/ui/components/PlanApproval.d.ts +0 -7
  220. package/dist/ui/components/PlanApproval.js +0 -60
  221. package/dist/ui/components/PlanApproval.js.map +0 -1
  222. package/dist/ui/components/PlanBanner.d.ts +0 -5
  223. package/dist/ui/components/PlanBanner.js +0 -31
  224. package/dist/ui/components/PlanBanner.js.map +0 -1
  225. package/dist/ui/components/PlanProgress.d.ts +0 -6
  226. package/dist/ui/components/PlanProgress.js +0 -30
  227. package/dist/ui/components/PlanProgress.js.map +0 -1
  228. package/dist/ui/hooks/useMinDisplayTime.d.ts +0 -10
  229. package/dist/ui/hooks/useMinDisplayTime.js +0 -34
  230. package/dist/ui/hooks/useMinDisplayTime.js.map +0 -1
  231. package/dist/ui/hooks/useSessionManager.d.ts +0 -12
  232. package/dist/ui/hooks/useSessionManager.js +0 -43
  233. package/dist/ui/hooks/useSessionManager.js.map +0 -1
  234. package/dist/ui/hooks/useSlashCommands.d.ts +0 -6
  235. package/dist/ui/hooks/useSlashCommands.js +0 -11
  236. package/dist/ui/hooks/useSlashCommands.js.map +0 -1
  237. package/dist/ui/terminal-history-spacing.d.ts +0 -1
  238. package/dist/ui/terminal-history-spacing.js +0 -2
  239. package/dist/ui/terminal-history-spacing.js.map +0 -1
  240. package/dist/ui/utils/markdown-cache.d.ts +0 -10
  241. package/dist/ui/utils/markdown-cache.js +0 -58
  242. package/dist/ui/utils/markdown-cache.js.map +0 -1
  243. package/dist/ui/utils/token-to-ansi.d.ts +0 -8
  244. package/dist/ui/utils/token-to-ansi.js +0 -323
  245. package/dist/ui/utils/token-to-ansi.js.map +0 -1
@@ -42,7 +42,7 @@ import { SettingsManager } from "./core/settings-manager.js";
42
42
  import { getModel, getDefaultThinkingLevel, getContextWindow, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
43
43
  import { resolveStartOrFallback } from "./core/resolve-start.js";
44
44
  import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
45
- import { getGitHubRepoSlug } from "./utils/github.js";
45
+ import { scoreProjectHealth } from "./core/project-health-score.js";
46
46
  import { createRepoPolls } from "./app-sidecar/repo-polls.js";
47
47
  import { extractPlanSteps } from "./utils/plan-steps.js";
48
48
  import { getSupportedThinkingLevels, isThinkingLevelSupported } from "./core/thinking-level.js";
@@ -54,7 +54,7 @@ import { installTerminationHandlers } from "./core/shutdown.js";
54
54
  import { stopRadio } from "./core/radio.js";
55
55
  import { enrichProcessPath } from "./core/shell-path.js";
56
56
  import { createElicitationBridge } from "./core/mcp/index.js";
57
- import { askSoftDeadlineMs, createAskUserBridge, deliverLateAnswer } from "./core/ask-user.js";
57
+ import { askSoftDeadlineMs, createAskUserBridge, deliverLateAnswer, } from "./core/ask-user.js";
58
58
  import { createAskUserTool } from "./tools/ask-user.js";
59
59
  import { createUsageService } from "./app-sidecar/usage.js";
60
60
  import { daemonReadBody, daemonJson, json } from "./app-sidecar/http.js";
@@ -64,6 +64,10 @@ import { ALL_PROVIDERS, loadProjectModelPrefs, loadNolanModelPref, loadAutopilot
64
64
  import { NOLAN_ALLOWED_TOOLS } from "./app-sidecar/nolan-context.js";
65
65
  import { createProgressManager } from "./app-sidecar/progress-manager.js";
66
66
  import { handleSessionRoutes } from "./app-sidecar/session-routes.js";
67
+ import { handleGoalRoutes } from "./app-sidecar/goal-routes.js";
68
+ import { readGitState, resolveGitCwd } from "./app-sidecar/git-target.js";
69
+ import { goalPhase, goalsRoot, isGoalDir, startGoalOnAnswer, readGoal, } from "./core/goals-store.js";
70
+ import { createGoalsAgentSession, goalCritterPick, loadGoalBriefExtras, } from "./goals-agent/goals-agent.js";
67
71
  import { handleWorkspaceRoutes } from "./app-sidecar/workspace-routes.js";
68
72
  import { handleHistoryRoutes } from "./app-sidecar/history-routes.js";
69
73
  import { handlePromptRoutes } from "./app-sidecar/prompt-routes.js";
@@ -176,6 +180,18 @@ async function main() {
176
180
  for (const ctx of sessions.values())
177
181
  ctx.broadcast(type, data);
178
182
  };
183
+ const sessionIdsAt = (cwd) => {
184
+ const target = path.resolve(cwd);
185
+ return [...sessions.values()]
186
+ .filter((ctx) => path.resolve(ctx.cwd) === target)
187
+ .map((ctx) => ctx.id)
188
+ .sort();
189
+ };
190
+ /** Whether any live window session in `cwd` is working right now. */
191
+ const busyAt = (cwd) => {
192
+ const target = path.resolve(cwd);
193
+ return [...sessions.values()].some((ctx) => path.resolve(ctx.cwd) === target && ctx.isBusy());
194
+ };
179
195
  // Providers currently mid-OAuth in some window. Daemon-level so two windows
180
196
  // cannot race two browser flows for the same provider into one auth file.
181
197
  const oauthInFlightProviders = new Set();
@@ -270,6 +286,8 @@ async function main() {
270
286
  broadcastAll,
271
287
  oauthInFlightProviders,
272
288
  repoPolls,
289
+ sessionIdsAt,
290
+ busyAt,
273
291
  }, { id, mode, chatAgent, cwd: sessionCwd, sessionPath });
274
292
  sessions.set(id, ctx);
275
293
  log("INFO", "app-sidecar", "session created", {
@@ -278,7 +296,13 @@ async function main() {
278
296
  chatAgent,
279
297
  cwd: sessionCwd,
280
298
  });
281
- daemonJson(res, 200, { sessionId: id });
299
+ // The conversation file too, so the app can reopen this exact
300
+ // session if the daemon restarts (a new session has no path yet
301
+ // on the app's side).
302
+ daemonJson(res, 200, {
303
+ sessionId: id,
304
+ sessionPath: ctx.session.getState().sessionPath ?? null,
305
+ });
282
306
  }
283
307
  catch (err) {
284
308
  const message = err instanceof Error ? err.message : String(err);
@@ -432,7 +456,7 @@ async function main() {
432
456
  * logger, PATH, shared auth file, and radio live at the daemon level.
433
457
  */
434
458
  async function createSession(deps, opts) {
435
- const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders, repoPolls, } = deps;
459
+ const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders, repoPolls, sessionIdsAt, busyAt, } = deps;
436
460
  const paths = deps.paths;
437
461
  const mode = opts.mode;
438
462
  let chatAgent = opts.chatAgent;
@@ -441,6 +465,10 @@ async function createSession(deps, opts) {
441
465
  // root; create it on first use so a fresh install can start a video at once.
442
466
  if (mode === "motion")
443
467
  await fs.mkdir(cwd, { recursive: true });
468
+ // A coding session whose cwd is a Goal folder is that goal's coordinator.
469
+ let goal = mode === "code" && isGoalDir(goalsRoot(paths.agentDir), cwd)
470
+ ? await readGoal(cwd).then((record) => (record ? { dir: cwd, record } : null))
471
+ : null;
444
472
  // Base host for parsing request-URL query params (value is irrelevant to
445
473
  // parsing); the daemon owns the real listen host.
446
474
  const host = "127.0.0.1";
@@ -586,7 +614,9 @@ async function createSession(deps, opts) {
586
614
  onClosed: (ids) => broadcast("ask_user_closed", { ids }),
587
615
  onLateAnswer: (late) => {
588
616
  log("INFO", "app-sidecar", "ask_user late answer queued", { id: late.prompt.id });
589
- deliverLateAnswer(late, {
617
+ // A late "Start the goal now" starts the goal before EZ Goals hears it,
618
+ // so the run it wakes into may already hand out work.
619
+ void startGoalFrom({ action: "answer", answers: late.answers }, late.prompt.questions).then(() => deliverLateAnswer(late, {
590
620
  queueMessage: (text) => session.queueMessage(text),
591
621
  isBusy: () => running || runClaim.active || autopilotActive,
592
622
  onQueued: () => broadcast("queued", {
@@ -594,13 +624,44 @@ async function createSession(deps, opts) {
594
624
  messages: session.listQueuedMessages(),
595
625
  }),
596
626
  startIdleRun: () => void runStrandedQueue(),
597
- });
627
+ }));
598
628
  },
599
629
  });
600
- const askUserTool = createAskUserTool(asks.park);
630
+ // A goal starts when the user picks "Start the goal now" from EZ Goals'
631
+ // question: answered while it waits, or late, after its deadline (see
632
+ // onLateAnswer). Recorded once, and announced so the window can switch to
633
+ // the goal's world view. Never throws: a failed write leaves it briefing.
634
+ const startGoalFrom = async (result, questions) => {
635
+ const current = goal;
636
+ if (!current)
637
+ return;
638
+ const started = await startGoalOnAnswer(current.dir, current.record, result, new Date(), questions).catch((err) => {
639
+ log("WARN", "app-sidecar", "couldn't record goal start", {
640
+ error: err instanceof Error ? err.message : String(err),
641
+ });
642
+ return null;
643
+ });
644
+ if (started) {
645
+ goal = { dir: current.dir, record: started };
646
+ broadcast("goal_state", { id: started.id, phase: goalPhase(started) });
647
+ }
648
+ };
649
+ const askUserTool = createAskUserTool(async (request) => {
650
+ const result = await asks.park(request);
651
+ await startGoalFrom(result, request.questions);
652
+ return result;
653
+ });
601
654
  // The session file path to resume (passed by the daemon's POST /session);
602
- // empty/unset starts a fresh session.
603
- const resumeSessionPath = opts.sessionPath;
655
+ // empty/unset starts a fresh session. A file that's gone (retention cleaned
656
+ // it up, or its goal was deleted) starts fresh too: failing instead left
657
+ // the window with no session at all, so not even its goal list loaded.
658
+ const resumeSessionPath = opts.sessionPath &&
659
+ (await fs.access(opts.sessionPath).then(() => true, () => false))
660
+ ? opts.sessionPath
661
+ : undefined;
662
+ if (opts.sessionPath && !resumeSessionPath) {
663
+ log("WARN", "app-sidecar", "session file gone, starting fresh", { cwd });
664
+ }
604
665
  let abort = new AbortController();
605
666
  const baseSessionOptions = {
606
667
  provider,
@@ -647,6 +708,19 @@ async function createSession(deps, opts) {
647
708
  additionalTools: [askUserTool],
648
709
  });
649
710
  }
711
+ else if (goal) {
712
+ // A Goal window talks to EZ Goals, the orchestrator, not the coding agent.
713
+ session = createGoalsAgentSession({
714
+ ...baseSessionOptions,
715
+ goal,
716
+ // Memory and the attached projects' rules, read once for this session.
717
+ extras: await loadGoalBriefExtras(goal),
718
+ // Read live: the goal turns active mid-session when the user starts it.
719
+ isActive: () => Boolean(goal?.record.startedAt),
720
+ getChildModel: () => goalCritterPick(goal?.record),
721
+ additionalTools: [askUserTool],
722
+ });
723
+ }
650
724
  else {
651
725
  session = new AgentSession({
652
726
  ...baseSessionOptions,
@@ -685,6 +759,18 @@ async function createSession(deps, opts) {
685
759
  });
686
760
  }
687
761
  await session.initialize();
762
+ // A goal's attached folders join its workspace so the coordinator can read
763
+ // them. A resumed session may already carry them; `addDirectory` skips those.
764
+ if (goal) {
765
+ for (const item of goal.record.context) {
766
+ if (item.kind !== "folder")
767
+ continue;
768
+ const added = await session.addDirectory(item.path);
769
+ if (!added.ok && !added.error.startsWith("Already in the workspace")) {
770
+ log("WARN", "app-sidecar", "goal context folder unavailable", { error: added.error });
771
+ }
772
+ }
773
+ }
688
774
  if (mode === "chat") {
689
775
  const restoredAgent = [...session.getAppMarkers()]
690
776
  .reverse()
@@ -798,23 +884,38 @@ async function createSession(deps, opts) {
798
884
  });
799
885
  }
800
886
  // Workspace extras (context window, git status, background tasks). Git state
801
- // is resolved once at startup and refreshed after every run; the context
802
- // window follows the active model.
803
- const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount, initialGitHubSlug] = await Promise.all([
804
- getGitBranch(cwd).catch(() => null),
805
- isGitRepo(cwd).catch(() => false),
806
- getGitDirtyFileCount(cwd).catch(() => 0),
807
- getGitHubRepoSlug(cwd).catch(() => null),
808
- ]);
809
- let gitBranch = initialGitBranch;
810
- let gitIsRepo = initialGitIsRepo;
811
- let gitDirtyFileCount = initialDirtyFileCount;
887
+ // is resolved at startup and refreshed after every run; the context window
888
+ // follows the active model. A Goal window reads git from its attached
889
+ // project folder, not the goal folder (see resolveGitCwd), and re-targets
890
+ // when the goal's folders change (`retargetGit`).
891
+ let gitCwd = await resolveGitCwd(cwd, goal?.record ?? null);
892
+ const initialGit = await readGitState(gitCwd);
893
+ let gitBranch = initialGit.branch;
894
+ let gitIsRepo = initialGit.isRepo;
895
+ let gitDirtyFileCount = initialGit.dirtyFileCount;
812
896
  // Open issue/PR counts for the origin repo's GitHub slug, via the `gh` CLI's
813
897
  // auth. null = unknown (gh missing/unauthed, non-GitHub origin) → chips hidden.
814
- const gitHubSlug = initialGitHubSlug;
898
+ let gitHubSlug = initialGit.gitHubSlug;
815
899
  let gitHubIssues = null;
816
900
  let gitHubPRs = null;
817
901
  let gitHubCI = null;
902
+ // Latest Project Health scan (code mode only); scored with the live CI result.
903
+ let projectHealthScan = null;
904
+ // Footer extras go out on every git/CI/task change; score only when the scan
905
+ // or CI result actually changed.
906
+ let scoredHealth = null;
907
+ function currentProjectHealth() {
908
+ if (!projectHealthScan)
909
+ return null;
910
+ if (scoredHealth?.scan !== projectHealthScan || scoredHealth.ci !== gitHubCI) {
911
+ scoredHealth = {
912
+ scan: projectHealthScan,
913
+ ci: gitHubCI,
914
+ health: scoreProjectHealth(projectHealthScan, gitHubCI),
915
+ };
916
+ }
917
+ return scoredHealth.health;
918
+ }
818
919
  function currentContextWindow() {
819
920
  const st = session.getState();
820
921
  return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
@@ -831,6 +932,7 @@ async function createSession(deps, opts) {
831
932
  gitHubPRs,
832
933
  gitHubCI,
833
934
  gitHubRepoUrl: gitHubSlug ? `https://github.com/${gitHubSlug}` : null,
935
+ projectHealth: currentProjectHealth(),
834
936
  tasks: session.listBackgroundProcesses(),
835
937
  // Roots added with /add-dir — the header shows a badge when non-empty.
836
938
  additionalRoots: session.getAdditionalRoots(),
@@ -1404,7 +1506,11 @@ async function createSession(deps, opts) {
1404
1506
  }
1405
1507
  finally {
1406
1508
  const cancelled = runLifecycle.isCancellationRequested(generation);
1407
- const verificationProblem = cancelled ? null : session.getVerificationProblem();
1509
+ // A plain reply has no real end (the user keeps building), so checks are
1510
+ // left to the commit and unchecked edits are not "Unverified". Only an
1511
+ // Autopilot cycle, which does end, reports unchecked work. A check that
1512
+ // ran and failed still shows as failed (from the evidence).
1513
+ const verificationProblem = cancelled || !autopilotActive ? null : session.getVerificationProblem();
1408
1514
  if (runSucceeded && verificationProblem && ownsGeneration) {
1409
1515
  // Expected control outcome: run_end and the journal already carry Unverified.
1410
1516
  // Do not format it as a crash or persist a misleading error marker.
@@ -1421,14 +1527,15 @@ async function createSession(deps, opts) {
1421
1527
  // A run may have switched branches, changed files, or spawned/finished
1422
1528
  // background tasks. Refresh the workspace extras once it settles.
1423
1529
  [gitBranch, gitIsRepo, gitDirtyFileCount] = await Promise.all([
1424
- getGitBranch(cwd).catch(() => gitBranch),
1425
- isGitRepo(cwd).catch(() => gitIsRepo),
1426
- getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
1530
+ getGitBranch(gitCwd).catch(() => gitBranch),
1531
+ isGitRepo(gitCwd).catch(() => gitIsRepo),
1532
+ getGitDirtyFileCount(gitCwd).catch(() => gitDirtyFileCount),
1427
1533
  ]);
1428
1534
  // A run may have opened/closed issues or PRs — refresh fire-and-forget so
1429
1535
  // teardown isn't delayed by the network. Broadcasts itself on change.
1430
- gitHubCountsPoll?.refresh();
1431
- ciPoll.refresh();
1536
+ repoWatch.gitHubCounts?.refresh();
1537
+ repoWatch.ci.refresh();
1538
+ repoWatch.health?.refresh();
1432
1539
  // Serialize behind any marker/tool-triggered refresh so the terminal
1433
1540
  // progress snapshot uses the live plan file. Once every canonical step
1434
1541
  // is complete, remove the approved plan from future system prompts and
@@ -1771,30 +1878,82 @@ async function createSession(deps, opts) {
1771
1878
  // (they change outside the agent: editor saves, commits, teammates). Branch/
1772
1879
  // repo state already refreshes after agent runs. The pollers are shared with
1773
1880
  // every other window on the same repo; each window broadcasts only changes.
1774
- const repoKey = path.resolve(cwd);
1775
- const dirtyFilesPoll = repoPolls.dirtyFiles.subscribe(repoKey, (next) => {
1776
- if (next === gitDirtyFileCount)
1777
- return;
1778
- gitDirtyFileCount = next;
1779
- broadcast("extras", footerExtras());
1780
- });
1781
- // Failed checks publish nothing, so the chips keep their last-known numbers
1782
- // instead of flickering off on a timeout. None when origin isn't GitHub.
1783
- const gitHubCountsPoll = gitHubSlug
1784
- ? repoPolls.gitHubCounts.subscribe(gitHubSlug, (counts) => {
1785
- if (counts.issues === gitHubIssues && counts.prs === gitHubPRs)
1881
+ function watchRepo(dir) {
1882
+ const repoKey = path.resolve(dir);
1883
+ const dirtyFiles = repoPolls.dirtyFiles.subscribe(repoKey, (next) => {
1884
+ if (next === gitDirtyFileCount)
1786
1885
  return;
1787
- gitHubIssues = counts.issues;
1788
- gitHubPRs = counts.prs;
1886
+ gitDirtyFileCount = next;
1789
1887
  broadcast("extras", footerExtras());
1790
- })
1791
- : null;
1792
- const ciPoll = repoPolls.gitHubCI.subscribe(repoKey, (next) => {
1793
- if (JSON.stringify(next) === JSON.stringify(gitHubCI))
1888
+ });
1889
+ // Failed checks publish nothing, so the chips keep their last-known numbers
1890
+ // instead of flickering off on a timeout. None when origin isn't GitHub.
1891
+ const gitHubCounts = gitHubSlug
1892
+ ? repoPolls.gitHubCounts.subscribe(gitHubSlug, (counts) => {
1893
+ if (counts.issues === gitHubIssues && counts.prs === gitHubPRs)
1894
+ return;
1895
+ gitHubIssues = counts.issues;
1896
+ gitHubPRs = counts.prs;
1897
+ broadcast("extras", footerExtras());
1898
+ })
1899
+ : null;
1900
+ const ci = repoPolls.gitHubCI.subscribe(repoKey, (next) => {
1901
+ if (JSON.stringify(next) === JSON.stringify(gitHubCI))
1902
+ return;
1903
+ gitHubCI = next;
1904
+ broadcast("extras", footerExtras());
1905
+ });
1906
+ // Project Health is a code-workspace signal; chat and motion windows skip the scan.
1907
+ const health = mode === "code"
1908
+ ? repoPolls.projectHealth.subscribe(repoKey, (next) => {
1909
+ if (JSON.stringify(next) === JSON.stringify(projectHealthScan))
1910
+ return;
1911
+ projectHealthScan = next;
1912
+ broadcast("extras", footerExtras());
1913
+ })
1914
+ : null;
1915
+ return {
1916
+ gitHubCounts,
1917
+ ci,
1918
+ health,
1919
+ unsubscribe() {
1920
+ dirtyFiles.unsubscribe();
1921
+ gitHubCounts?.unsubscribe();
1922
+ ci.unsubscribe();
1923
+ health?.unsubscribe();
1924
+ },
1925
+ };
1926
+ }
1927
+ let repoWatch = watchRepo(gitCwd);
1928
+ let repoWatchStopped = false;
1929
+ let gitTargetGeneration = 0;
1930
+ /**
1931
+ * A goal's attached folders changed: point the titlebar's git state at the
1932
+ * new project folder. No-op when the target is unchanged; a newer call or
1933
+ * teardown supersedes one still reading git.
1934
+ */
1935
+ async function retargetGit() {
1936
+ const generation = ++gitTargetGeneration;
1937
+ const next = await resolveGitCwd(cwd, goal?.record ?? null);
1938
+ if (generation !== gitTargetGeneration || repoWatchStopped || next === gitCwd)
1939
+ return;
1940
+ const state = await readGitState(next);
1941
+ if (generation !== gitTargetGeneration || repoWatchStopped)
1794
1942
  return;
1795
- gitHubCI = next;
1943
+ repoWatch.unsubscribe();
1944
+ gitCwd = next;
1945
+ gitBranch = state.branch;
1946
+ gitIsRepo = state.isRepo;
1947
+ gitDirtyFileCount = state.dirtyFileCount;
1948
+ gitHubSlug = state.gitHubSlug;
1949
+ // The old repo's numbers don't apply; the new watchers fill these in.
1950
+ gitHubIssues = null;
1951
+ gitHubPRs = null;
1952
+ gitHubCI = null;
1953
+ projectHealthScan = null;
1954
+ repoWatch = watchRepo(gitCwd);
1796
1955
  broadcast("extras", footerExtras());
1797
- });
1956
+ }
1798
1957
  const routeCtx = {
1799
1958
  get session() {
1800
1959
  return session;
@@ -1846,6 +2005,23 @@ async function createSession(deps, opts) {
1846
2005
  cwd,
1847
2006
  asks,
1848
2007
  runClaim,
2008
+ sessionId: opts.id,
2009
+ sessionIdsAt,
2010
+ busyAt,
2011
+ get goal() {
2012
+ return goal;
2013
+ },
2014
+ set goal(v) {
2015
+ const foldersBefore = JSON.stringify(goal?.record.context ?? []);
2016
+ goal = v;
2017
+ if (JSON.stringify(goal?.record.context ?? []) !== foldersBefore) {
2018
+ retargetGit().catch((err) => {
2019
+ log("WARN", "app-sidecar", "goal git retarget failed", {
2020
+ message: err instanceof Error ? err.message : String(err),
2021
+ });
2022
+ });
2023
+ }
2024
+ },
1849
2025
  broadcast,
1850
2026
  get scheduledRunActive() {
1851
2027
  return scheduledRunActive;
@@ -2003,6 +2179,8 @@ async function createSession(deps, opts) {
2003
2179
  return;
2004
2180
  if (handleWorkspaceRoutes(routeCtx, req, res, url, method))
2005
2181
  return;
2182
+ if (handleGoalRoutes(routeCtx, req, res, url, method))
2183
+ return;
2006
2184
  if (handleHistoryRoutes(routeCtx, req, res, url, method))
2007
2185
  return;
2008
2186
  if (handlePromptRoutes(routeCtx, req, res, url, method))
@@ -2020,6 +2198,12 @@ async function createSession(deps, opts) {
2020
2198
  json(res, 404, { error: "not found" });
2021
2199
  }
2022
2200
  async function dispose() {
2201
+ // A window closed (or switched project) mid-run: stop the run itself, any
2202
+ // run-all sweep or autopilot turn, and a model download this window
2203
+ // started. Otherwise they keep streaming tokens and running tools on a
2204
+ // session that is being torn down.
2205
+ abortOwnedWork();
2206
+ cancelHfPull();
2023
2207
  releaseRunAwake?.();
2024
2208
  releaseRunAwake = null;
2025
2209
  releaseSubagentAwake?.();
@@ -2029,9 +2213,8 @@ async function createSession(deps, opts) {
2029
2213
  tasksPollStopped = true;
2030
2214
  if (tasksPoll)
2031
2215
  clearTimeout(tasksPoll);
2032
- dirtyFilesPoll.unsubscribe();
2033
- gitHubCountsPoll?.unsubscribe();
2034
- ciPoll.unsubscribe();
2216
+ repoWatchStopped = true;
2217
+ repoWatch.unsubscribe();
2035
2218
  // Stop the Telegram serve loop + dispose its per-chat sessions.
2036
2219
  if (serveController)
2037
2220
  await serveController.stop().catch(() => { });
@@ -2062,6 +2245,10 @@ async function createSession(deps, opts) {
2062
2245
  handle,
2063
2246
  dispose,
2064
2247
  stopBackgroundProcesses,
2248
+ isBusy: () => running ||
2249
+ runClaim.active ||
2250
+ autopilotActive ||
2251
+ session.listSubAgents().some((w) => w.state === "running" || w.state === "starting"),
2065
2252
  };
2066
2253
  }
2067
2254
  main().catch(async (err) => {