@capagents/forgeqe 1.3.1 → 2.0.0

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 (119) hide show
  1. package/HOW_TO_USE.md +373 -9
  2. package/README.md +55 -4
  3. package/docs/ARCHITECTURE.md +62 -1
  4. package/docs/CONTAINERS.md +227 -0
  5. package/docs/COST_ARCHITECTURE.md +32 -0
  6. package/docs/DESKTOP-CI.md +87 -0
  7. package/docs/DESKTOP.md +387 -0
  8. package/docs/MOBILE.md +228 -0
  9. package/docs/feature-test-prompts.md +25 -0
  10. package/docs/security-data-flow.html +440 -3
  11. package/docs/user-guide.html +797 -415
  12. package/env.example.yaml +67 -0
  13. package/forgeqe.example.yaml +56 -1
  14. package/llms.example.json +7 -0
  15. package/package.json +4 -1
  16. package/prompts/brain.md +31 -0
  17. package/prompts/find-tests.md +1 -0
  18. package/py/forgeqe_desktop/__init__.py +1 -0
  19. package/py/forgeqe_desktop/__main__.py +3 -0
  20. package/py/forgeqe_desktop/apps.py +80 -0
  21. package/py/forgeqe_desktop/checks.py +168 -0
  22. package/py/forgeqe_desktop/cli.py +90 -0
  23. package/py/forgeqe_desktop/handhooks.py +293 -0
  24. package/py/forgeqe_desktop/handrecord.py +239 -0
  25. package/py/forgeqe_desktop/heal.py +107 -0
  26. package/py/forgeqe_desktop/models.py +208 -0
  27. package/py/forgeqe_desktop/pywinauto_driver.py +217 -0
  28. package/py/forgeqe_desktop/record.py +150 -0
  29. package/py/forgeqe_desktop/replay.py +345 -0
  30. package/py/forgeqe_desktop/service.py +356 -0
  31. package/py/forgeqe_desktop/session.py +60 -0
  32. package/py/forgeqe_desktop/ufo_bridge.py +86 -0
  33. package/py/forgeqe_desktop/ufo_runner.py +305 -0
  34. package/py/forgeqe_desktop/visual.py +112 -0
  35. package/py/forgeqe_desktop/workflow.py +338 -0
  36. package/py/forgeqe_engine/cli.py +10 -0
  37. package/py/forgeqe_engine/llm.py +54 -4
  38. package/py/forgeqe_engine/model_params.py +169 -0
  39. package/py/forgeqe_engine/summary.py +6 -0
  40. package/py/forgeqe_mobile/__init__.py +1 -0
  41. package/py/forgeqe_mobile/__main__.py +3 -0
  42. package/py/forgeqe_mobile/appium_driver.py +342 -0
  43. package/py/forgeqe_mobile/apps.py +84 -0
  44. package/py/forgeqe_mobile/cli.py +70 -0
  45. package/py/forgeqe_mobile/explore.py +203 -0
  46. package/py/forgeqe_mobile/hyperexecute.py +261 -0
  47. package/py/forgeqe_mobile/maestro.py +321 -0
  48. package/py/forgeqe_mobile/service.py +332 -0
  49. package/skills/testcontainers/SKILL.md +42 -0
  50. package/src/agents.ts +11 -2
  51. package/src/api/plan.ts +2 -0
  52. package/src/api/write.ts +2 -0
  53. package/src/auth/copilot.ts +130 -0
  54. package/src/auth/signin.ts +54 -1
  55. package/src/brain/index.ts +26 -4
  56. package/src/capabilities/guide.ts +2 -0
  57. package/src/capabilities/registry.ts +110 -2
  58. package/src/capabilities/status.ts +15 -0
  59. package/src/cases/defects.ts +2 -1
  60. package/src/cases/rca.ts +2 -0
  61. package/src/childEnv.ts +4 -1
  62. package/src/cli/args.ts +3 -2
  63. package/src/cli/help.ts +24 -3
  64. package/src/cli/index.ts +47 -3
  65. package/src/code/find.ts +26 -0
  66. package/src/code/loop.ts +4 -1
  67. package/src/code/prompt.ts +2 -1
  68. package/src/code/prreview.ts +2 -0
  69. package/src/code/review.ts +6 -2
  70. package/src/code/runtime.ts +34 -3
  71. package/src/commands.ts +6 -1
  72. package/src/config.ts +159 -3
  73. package/src/containers/command.ts +68 -0
  74. package/src/containers/compose.ts +173 -0
  75. package/src/containers/config.ts +283 -0
  76. package/src/containers/mock.ts +156 -0
  77. package/src/containers/run.ts +362 -0
  78. package/src/containers/tests.ts +80 -0
  79. package/src/context.ts +33 -6
  80. package/src/copilot.ts +155 -0
  81. package/src/desktop/bridge.ts +229 -0
  82. package/src/desktop/command.ts +350 -0
  83. package/src/desktop/edit.ts +217 -0
  84. package/src/desktop/junit.ts +35 -0
  85. package/src/desktop/report.ts +155 -0
  86. package/src/desktop/run.ts +252 -0
  87. package/src/desktop/setup.ts +134 -0
  88. package/src/engine/bridge.ts +12 -5
  89. package/src/engine/setup.ts +6 -3
  90. package/src/environments.ts +566 -0
  91. package/src/evals/domain.ts +1 -0
  92. package/src/explore/charters.ts +2 -0
  93. package/src/guardrails.ts +145 -0
  94. package/src/heal/loop.ts +4 -1
  95. package/src/heal/prompt.ts +2 -1
  96. package/src/init.ts +5 -2
  97. package/src/llm.ts +23 -2
  98. package/src/mcp/attachtools.ts +16 -4
  99. package/src/mcp/captools.ts +27 -0
  100. package/src/mcp/containertools.ts +212 -0
  101. package/src/mcp/desktoptools.ts +334 -0
  102. package/src/mcp/mobiletools.ts +132 -0
  103. package/src/mcp/reporttools.ts +17 -2
  104. package/src/mcp/repotests.ts +22 -3
  105. package/src/mcp/routing.ts +11 -5
  106. package/src/mcp/scope.ts +210 -0
  107. package/src/mcp/server.ts +80 -5
  108. package/src/mcp/tools.ts +38 -1
  109. package/src/mobile/command.ts +245 -0
  110. package/src/mobile/run.ts +226 -0
  111. package/src/pack/pipeline.ts +44 -2
  112. package/src/refine.ts +3 -0
  113. package/src/report/html.ts +3 -0
  114. package/src/report/template.html +48 -48
  115. package/src/run.ts +7 -3
  116. package/src/tokenomics/ledger.ts +3 -1
  117. package/src/tui/App.tsx +23 -0
  118. package/src/update.ts +118 -8
  119. package/src/usage.ts +1 -1
package/src/run.ts CHANGED
@@ -4,6 +4,7 @@ import { createRequire } from "node:module"
4
4
  import path from "node:path"
5
5
 
6
6
  import { assertGoalUrlsAllowed, hostAllowed, normalizeHttpUrl } from "./guard.ts"
7
+ import { ghAuthToken } from "./cases/github.ts"
7
8
  import { missingKey } from "./llm.ts"
8
9
  import type { AgentEvent, ReplayMode, RunResult } from "./types.ts"
9
10
  import { browserConfigFor, type AppConfig, type BrowserProvider } from "./config.ts"
@@ -21,7 +22,9 @@ import { historyFile, recordQuality } from "./quality/history.ts"
21
22
  import { compareVisual, engineVisual } from "./quality/visual.ts"
22
23
  import { enginePrompts, prompt } from "./prompts.ts"
23
24
  import { knowledgeBlock, readKnowledge, relevantKnowledge } from "./knowledge.ts"
25
+ import { environmentBrief } from "./environments.ts"
24
26
  import { registryMode } from "./registry/index.ts"
27
+ import { guardrailsNotes } from "./guardrails.ts"
25
28
 
26
29
  export type ExecuteOptions = {
27
30
  config: AppConfig
@@ -211,7 +214,7 @@ export async function executeRun(opts: ExecuteOptions): Promise<{ result: RunRes
211
214
  return withReport(out)
212
215
  }
213
216
 
214
- const missing = missingKey(opts.config.llm)
217
+ const missing = missingKey(opts.config.llm, "engine", ghAuthToken)
215
218
  if (missing) throw new Error(missing)
216
219
  // A remote grid's keys are checked here, before the engine starts, rather than failing inside it.
217
220
  if (browser.remote) {
@@ -220,9 +223,10 @@ export async function executeRun(opts: ExecuteOptions): Promise<{ result: RunRes
220
223
  }
221
224
  const log = (text: string) => opts.onEvent?.({ type: "log", level: "info", text })
222
225
  const python = await ensureEngine(log)
223
- // The browser agent's added instructions: forgeqe.yaml's, prompts/browser-extra.md, and the product knowledge that matches the goal.
226
+ // The browser agent's added instructions: forgeqe.yaml's, prompts/browser-extra.md, the product knowledge that matches the goal, and the guardrails.
224
227
  const knowledge = opts.config.knowledge && opts.config.knowledge.browserMaxChars > 0 ? knowledgeBlock(relevantKnowledge(readKnowledge(opts.config.knowledge.dir), goal, opts.config.knowledge.browserMaxChars, 2)) : ""
225
- const extend = [opts.config.prompts.guiderails, opts.config.prompts.stepInstruction, prompt("browser-extra"), knowledge]
228
+ // The team's guardrails come last, so nothing added after them reads as overriding them.
229
+ const extend = [opts.config.prompts.stepInstruction, prompt("browser-extra"), knowledge, environmentBrief(opts.config, "browser"), guardrailsNotes(opts.config) || ""]
226
230
  .map((s) => s.trim())
227
231
  .filter(Boolean)
228
232
  .join("\n\n")
@@ -18,7 +18,7 @@ import { costOf, recordUsage, type Price, type Role } from "../usage.ts"
18
18
  export type Trigger = "tui" | "cli" | "mcp" | "ci" | "monitor" | "schedule"
19
19
 
20
20
  /** What a call was for, from the tools it asked for: no tools is the model thinking or answering. */
21
- export type Purpose = "reasoning" | "planning" | "search" | "file read" | "edit" | "shell" | "browser" | "integration" | "delegation" | "unattributed"
21
+ export type Purpose = "reasoning" | "planning" | "search" | "file read" | "edit" | "shell" | "browser" | "desktop" | "mobile" | "integration" | "delegation" | "unattributed"
22
22
 
23
23
  /** What ForgeQE was doing when the call was made. Set with withUsageScope; inner scopes add to outer ones. */
24
24
  export type UsageScope = {
@@ -130,6 +130,8 @@ export function purposeOf(tools: string[] | undefined): Purpose {
130
130
  const DEFAULT_JOB: Record<Role, string> = {
131
131
  "ForgeQE's agent": "chat",
132
132
  "browser agent": "browser test",
133
+ "desktop agent": "desktop test",
134
+ "mobile agent": "mobile test",
133
135
  Scribe: "write tests",
134
136
  reviewer: "review",
135
137
  judge: "eval",
package/src/tui/App.tsx CHANGED
@@ -15,6 +15,7 @@ import { FAILURE_ACTION, FAILURE_LABEL } from "../cases/failure.ts"
15
15
  import { authStatus, openInBrowser, signIn, signOut, type AuthIO } from "../auth/signin.ts"
16
16
  import { HELP, parseCommand } from "../commands.ts"
17
17
  import { loadConfig, selectProfile, type AppConfig, type BrowserProvider } from "../config.ts"
18
+ import { appsText, exportEnvironment, withEnvironment } from "../environments.ts"
18
19
  import { TOOLS, type AskQuestion, type ToolEvent } from "../mcp/tools.ts"
19
20
  import type { executeRun } from "../run.ts"
20
21
  import { bundleLogs, logEvent } from "../sessionLog.ts"
@@ -1443,6 +1444,28 @@ export function App({ options }: { options: TuiOptions }) {
1443
1444
  }
1444
1445
  return
1445
1446
  }
1447
+ if (cmd.type === "env") {
1448
+ try {
1449
+ const cfg = configRef.current
1450
+ if (!cfg) throw new Error("Config is not loaded.")
1451
+ if (!cmd.name) {
1452
+ push("sys", appsText(cfg))
1453
+ return
1454
+ }
1455
+ configRef.current = withEnvironment(cfg, cmd.name)
1456
+ exportEnvironment(configRef.current)
1457
+ // Reloads in this session (/init, a profile switch) keep the environment.
1458
+ process.env.FORGEQE_ENV = cmd.name
1459
+ push("sys", `environment ${cmd.name}`)
1460
+ if (brainRef.current) {
1461
+ closeBrain()
1462
+ push("sys", "The agent switches to this environment on your next message and keeps the conversation.")
1463
+ }
1464
+ } catch (err) {
1465
+ push("err", (err as Error).message)
1466
+ }
1467
+ return
1468
+ }
1446
1469
  if (cmd.type === "init") {
1447
1470
  // forgeqe init in this folder, then the session reads what it wrote: the start screen updates at once.
1448
1471
  try {
package/src/update.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { spawnSync } from "node:child_process"
2
- import { existsSync, readFileSync, realpathSync, statSync } from "node:fs"
1
+ import { spawn, spawnSync } from "node:child_process"
2
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
3
4
  import path from "node:path"
4
5
  import { packageVersion } from "./tui/theme.ts"
5
6
 
@@ -28,19 +29,50 @@ export interface UpdateDeps {
28
29
  platform: NodeJS.Platform
29
30
  sleep: (ms: number) => Promise<void>
30
31
  now: () => number
32
+ /**
33
+ * Windows: starts `script` (a .cmd) detached, to run once this process has exited, and returns its log file.
34
+ * A running ForgeQE keeps files of its own install open (its launcher, native libraries), and Windows does not
35
+ * let an installer replace a file that is in use, so the update cannot run from inside ForgeQE.
36
+ */
37
+ defer?: (script: string) => string
38
+ }
39
+
40
+ /**
41
+ * How to start `cmd` on `platform`. Windows runs .cmd and .bat files (npm, pyenv-win) only through a shell, which Node
42
+ * refuses to do on its own (EINVAL), so those go through cmd.exe with each argument quoted.
43
+ */
44
+ export function spawnPlan(cmd: string, args: string[], platform: NodeJS.Platform): { cmd: string; args: string[]; shell: boolean } {
45
+ if (platform !== "win32" || /\.exe$/i.test(cmd)) return { cmd, args, shell: false }
46
+ return { cmd: winQuote(cmd), args: args.map(winQuote), shell: true }
47
+ }
48
+
49
+ /** One argument for cmd.exe: quoted when it has a space or a character cmd.exe would read. */
50
+ export function winQuote(a: string): string {
51
+ return /[\s&|<>^()%!"]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a
31
52
  }
32
53
 
33
54
  export function defaultDeps(): UpdateDeps {
34
55
  return {
35
56
  fetch,
36
57
  run: (cmd, args) => {
37
- const r = spawnSync(cmd, args, { encoding: "utf8" })
58
+ const plan = spawnPlan(cmd, args, process.platform)
59
+ const r = spawnSync(plan.cmd, plan.args, { encoding: "utf8", shell: plan.shell, windowsHide: true })
38
60
  return { status: r.error ? null : r.status, stdout: r.stdout || "", stderr: r.stderr || (r.error ? String(r.error.message) : "") }
39
61
  },
40
62
  env: process.env,
41
63
  platform: process.platform,
42
64
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
43
65
  now: () => Date.now(),
66
+ defer: (script) => {
67
+ const dir = path.join(tmpdir(), "forgeqe-update")
68
+ mkdirSync(dir, { recursive: true })
69
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-")
70
+ const file = path.join(dir, `update-${stamp}.cmd`)
71
+ const log = path.join(dir, `update-${stamp}.log`)
72
+ writeFileSync(file, script.replace(/\{LOG\}/g, log).replace(/\n/g, "\r\n"))
73
+ spawn("cmd.exe", ["/d", "/c", file], { detached: true, stdio: "ignore", windowsHide: true }).unref()
74
+ return log
75
+ },
44
76
  }
45
77
  }
46
78
 
@@ -142,12 +174,33 @@ function packageDirOf(file: string): string | undefined {
142
174
  return undefined
143
175
  }
144
176
 
177
+ /**
178
+ * The package a `forgeqe` on PATH belongs to when it is a copy or a wrapper, not a link: npm on Windows writes
179
+ * forgeqe.cmd next to node_modules; bun on Windows writes forgeqe.exe in ~/.bun/bin for ~/.bun/install/global; an npm
180
+ * prefix elsewhere keeps packages in <prefix>/lib/node_modules.
181
+ */
182
+ function packageOfShim(binDir: string): string | undefined {
183
+ const parts = NPM_PACKAGE.split("/")
184
+ for (const dir of [path.join(binDir, "node_modules", ...parts), path.join(binDir, "..", "install", "global", "node_modules", ...parts), path.join(binDir, "..", "lib", "node_modules", ...parts)]) {
185
+ const found = packageDirOf(path.join(dir, "package.json"))
186
+ if (found) return path.resolve(found)
187
+ }
188
+ return undefined
189
+ }
190
+
145
191
  function pythonOfScript(script: string, platform: NodeJS.Platform): string | undefined {
146
192
  if (platform === "win32") {
147
193
  const dir = path.dirname(script)
148
- if (path.basename(dir).toLowerCase() !== "scripts") return undefined
149
194
  const python = path.join(path.dirname(dir), "python.exe")
150
- return existsSync(python) ? python : undefined
195
+ if (path.basename(dir).toLowerCase() === "scripts" && existsSync(python)) return python
196
+ // pip's forgeqe.exe carries the Python it was installed for (`#!C:\...\python.exe` near its end): the way to find
197
+ // it for `pip install --user`, whose Scripts folder is not next to python.exe.
198
+ try {
199
+ const text = readFileSync(script).toString("latin1")
200
+ const m = [...text.matchAll(/#!\s*"?([^"\r\n]*?pythonw?\.exe)"?/gi)].pop()
201
+ if (m && existsSync(m[1])) return m[1]
202
+ } catch {}
203
+ return undefined
151
204
  }
152
205
  let first = ""
153
206
  try {
@@ -182,7 +235,10 @@ export function findInstalls(deps: UpdateDeps): Install[] {
182
235
  }
183
236
  if (seen.has(target)) continue
184
237
  seen.add(target)
185
- const pkgDir = packageDirOf(target)
238
+ const pkgDir = packageDirOf(target) || packageOfShim(dir)
239
+ // npm on Windows writes forgeqe, forgeqe.cmd and forgeqe.ps1 for one install.
240
+ if (pkgDir && seen.has(`pkg:${pkgDir}`)) continue
241
+ if (pkgDir) seen.add(`pkg:${pkgDir}`)
186
242
  if (pkgDir) {
187
243
  let version = ""
188
244
  try {
@@ -233,6 +289,47 @@ export function commandLine([cmd, args]: [string, string[]]): string {
233
289
  return [cmd, ...args].map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(" ")
234
290
  }
235
291
 
292
+ /** What to do when an installer refuses for a reason the machine sets, not ForgeQE (Linux: root-owned npm, PEP 668). */
293
+ export function failureHint(kind: InstallKind, output: string, platform: NodeJS.Platform): string {
294
+ if (/EACCES|permission denied/i.test(output) && kind === "npm") {
295
+ return platform === "win32"
296
+ ? "npm's global folder is not writable: run the terminal as administrator, or install with bun (bun add -g @capagents/forgeqe)."
297
+ : "npm's global folder belongs to root: run sudo npm install -g @capagents/forgeqe, or give npm a folder of your own (npm config set prefix ~/.npm-global)."
298
+ }
299
+ if (/externally-managed-environment/i.test(output)) {
300
+ return "This Python is managed by the system and pip will not change it: install with pipx install forgeqe-cli, or in a virtual environment."
301
+ }
302
+ if (/EBUSY|EPERM|being used by another process/i.test(output)) {
303
+ return "A file of the install is in use: close every ForgeQE window and run forgeqe update again."
304
+ }
305
+ return ""
306
+ }
307
+
308
+ /**
309
+ * The .cmd that updates Windows installs after the ForgeQE that started it has exited: it waits for these processes
310
+ * (ForgeQE and its launcher), runs each install's commands, and writes everything to {LOG}.
311
+ */
312
+ export function windowsUpdateScript(steps: { kind: InstallKind; bin: string; commands: Array<[string, string[]]> }[], pids: number[], version: string): string {
313
+ const lines = ["@echo off", "setlocal", ":wait"]
314
+ for (const pid of pids.filter((p) => Number.isInteger(p) && p > 0)) {
315
+ // ping waits a second where timeout cannot (a detached script has no console); System32 paths, so Git's find
316
+ // or another tasklist on PATH is never the one that runs.
317
+ lines.push(`"%SystemRoot%\\System32\\tasklist.exe" /FI "PID eq ${pid}" 2>nul | "%SystemRoot%\\System32\\find.exe" " ${pid} " >nul && (ping -n 2 127.0.0.1 >nul & goto wait)`)
318
+ }
319
+ lines.push(`echo Updating ForgeQE to ${version} > "{LOG}"`)
320
+ for (const step of steps) {
321
+ lines.push(`echo. >> "{LOG}"`, `echo Updating the ${step.kind} install (${step.bin}) >> "{LOG}"`)
322
+ for (const [cmd, args] of step.commands) {
323
+ const line = [cmd, ...args].map(winQuote).join(" ")
324
+ const cacheStep = args[0] === "pm" && args[1] === "cache"
325
+ lines.push(`echo $ ${line.replace(/[&|<>^]/g, "^$&")} >> "{LOG}"`)
326
+ lines.push(`call ${line} >> "{LOG}" 2>&1${cacheStep ? "" : ` || echo FAILED: ${step.kind} install >> "{LOG}"`}`)
327
+ }
328
+ }
329
+ lines.push(`echo. >> "{LOG}"`, `echo Done. Open a new terminal and run: forgeqe --version >> "{LOG}"`)
330
+ return `${lines.join("\n")}\n`
331
+ }
332
+
236
333
  export interface UpdateOptions {
237
334
  /** A version to install instead of the latest. */
238
335
  version?: string
@@ -283,6 +380,16 @@ export async function update(opts: UpdateOptions = {}, deps: UpdateDeps = defaul
283
380
  }
284
381
  }
285
382
 
383
+ if (deps.platform === "win32" && deps.defer) {
384
+ // Windows will not replace files the running ForgeQE holds open, so the installs update once it has exited.
385
+ const steps = stale.map((inst) => ({ kind: inst.kind, bin: inst.bin, commands: updateCommands(inst, target, deps.env) }))
386
+ for (const step of steps) for (const c of step.commands) log(` $ ${commandLine(c)}`)
387
+ const logFile = deps.defer(windowsUpdateScript(steps, [process.pid, process.ppid], target))
388
+ log(`\nThe update to ${target} starts as soon as this command exits (Windows locks the files a running ForgeQE uses).`)
389
+ log(`It writes what it does to ${logFile}. Then open a new terminal and run forgeqe --version.`)
390
+ return 0
391
+ }
392
+
286
393
  let failed = 0
287
394
  for (const inst of stale) {
288
395
  log(`\nUpdating the ${inst.kind} install (${inst.bin})`)
@@ -293,7 +400,10 @@ export async function update(opts: UpdateOptions = {}, deps: UpdateDeps = defaul
293
400
  const cacheStep = c[1][0] === "pm" && c[1][1] === "cache"
294
401
  if (cacheStep) continue
295
402
  failed++
296
- log((r.stderr || r.stdout).trim().split("\n").slice(-5).map((l) => ` ${l}`).join("\n"))
403
+ const output = (r.stderr || r.stdout).trim()
404
+ log(output.split("\n").slice(-5).map((l) => ` ${l}`).join("\n"))
405
+ const hint = failureHint(inst.kind, output, deps.platform)
406
+ if (hint) log(` ${hint}`)
297
407
  break
298
408
  }
299
409
  }
@@ -306,6 +416,6 @@ export async function update(opts: UpdateOptions = {}, deps: UpdateDeps = defaul
306
416
  log(`\n${behind.length || failed} install(s) are still behind ${target}; the output above says why.`)
307
417
  return 1
308
418
  }
309
- log(`\nforgeqe ${target} is installed. Open a new terminal (or run hash -r) if the shell still finds the old one.`)
419
+ log(`\nforgeqe ${target} is installed. Open a new terminal${deps.platform === "win32" ? "" : " (or run hash -r)"} if the shell still finds the old one.`)
310
420
  return 0
311
421
  }
package/src/usage.ts CHANGED
@@ -98,7 +98,7 @@ export function usageLine(usage: Usage, cost?: Cost | null): string {
98
98
  // The meter: every model session reports here, by role
99
99
  // ---------------------------------------------------------------------------------------------
100
100
 
101
- export type Role = "ForgeQE's agent" | "browser agent" | "Scribe" | "reviewer" | "judge" | "refiner" | "root-cause analyst"
101
+ export type Role = "ForgeQE's agent" | "browser agent" | "desktop agent" | "mobile agent" | "Scribe" | "reviewer" | "judge" | "refiner" | "root-cause analyst"
102
102
 
103
103
  type Entry = { usage: Usage; cost?: Cost; priced: boolean }
104
104