ptbk 0.119.0-21 → 0.119.0-26

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/README.md +16 -9
  2. package/dist/browser/browser.d.ts +50 -36
  3. package/dist/browser/browser.js +162 -118
  4. package/dist/browser/browser.js.map +1 -1
  5. package/dist/browser/fleet.d.ts +59 -0
  6. package/dist/browser/fleet.js +148 -0
  7. package/dist/browser/fleet.js.map +1 -0
  8. package/dist/browser/recording.js +2 -2
  9. package/dist/browser/recording.js.map +1 -1
  10. package/dist/chats/conversation.d.ts +1 -1
  11. package/dist/chats/conversation.js +8 -6
  12. package/dist/chats/conversation.js.map +1 -1
  13. package/dist/chats/local.js +5 -5
  14. package/dist/chats/local.js.map +1 -1
  15. package/dist/cli/commands/add.js +1 -1
  16. package/dist/cli/commands/add.js.map +1 -1
  17. package/dist/cli/commands/boilerplates.js +1 -1
  18. package/dist/cli/commands/boilerplates.js.map +1 -1
  19. package/dist/cli/commands/find.js +1 -1
  20. package/dist/cli/commands/find.js.map +1 -1
  21. package/dist/cli/commands/fix.js +9 -4
  22. package/dist/cli/commands/fix.js.map +1 -1
  23. package/dist/cli/commands/init.d.ts +5 -0
  24. package/dist/cli/commands/init.js +279 -118
  25. package/dist/cli/commands/init.js.map +1 -1
  26. package/dist/cli/commands/migrate.js +1 -1
  27. package/dist/cli/commands/migrate.js.map +1 -1
  28. package/dist/cli/commands/ping.js +20 -12
  29. package/dist/cli/commands/ping.js.map +1 -1
  30. package/dist/cli/commands/plan.js +20 -12
  31. package/dist/cli/commands/plan.js.map +1 -1
  32. package/dist/cli/commands/recover.js +1 -1
  33. package/dist/cli/commands/recover.js.map +1 -1
  34. package/dist/cli/commands/restart.js +6 -5
  35. package/dist/cli/commands/restart.js.map +1 -1
  36. package/dist/cli/commands/run.js +26 -15
  37. package/dist/cli/commands/run.js.map +1 -1
  38. package/dist/cli/commands/start.js +11 -7
  39. package/dist/cli/commands/start.js.map +1 -1
  40. package/dist/cli/commands/tool.d.ts +19 -0
  41. package/dist/cli/commands/tool.js +320 -0
  42. package/dist/cli/commands/tool.js.map +1 -0
  43. package/dist/cli/commands/verify.js +1 -1
  44. package/dist/cli/commands/verify.js.map +1 -1
  45. package/dist/cli/common.d.ts +7 -1
  46. package/dist/cli/common.js +37 -20
  47. package/dist/cli/common.js.map +1 -1
  48. package/dist/cli/compatibility.d.ts +8 -0
  49. package/dist/cli/compatibility.js +32 -0
  50. package/dist/cli/compatibility.js.map +1 -0
  51. package/dist/cli/parser.js +7 -3
  52. package/dist/cli/parser.js.map +1 -1
  53. package/dist/cli/registry.js +182 -7
  54. package/dist/cli/registry.js.map +1 -1
  55. package/dist/cli/service-view.js +1 -1
  56. package/dist/cli/service-view.js.map +1 -1
  57. package/dist/cli/sources.js +2 -0
  58. package/dist/cli/sources.js.map +1 -1
  59. package/dist/cli/spec.js +8 -6
  60. package/dist/cli/spec.js.map +1 -1
  61. package/dist/common/naming.d.ts +53 -0
  62. package/dist/common/naming.js +90 -0
  63. package/dist/common/naming.js.map +1 -0
  64. package/dist/engine/agenda.d.ts +70 -9
  65. package/dist/engine/agenda.js +294 -61
  66. package/dist/engine/agenda.js.map +1 -1
  67. package/dist/engine/executor.d.ts +5 -0
  68. package/dist/engine/executor.js +24 -11
  69. package/dist/engine/executor.js.map +1 -1
  70. package/dist/engine/harnesses.d.ts +82 -18
  71. package/dist/engine/harnesses.js +191 -39
  72. package/dist/engine/harnesses.js.map +1 -1
  73. package/dist/engine/preview.js +2 -2
  74. package/dist/engine/preview.js.map +1 -1
  75. package/dist/engine/routing.d.ts +10 -0
  76. package/dist/engine/routing.js +67 -28
  77. package/dist/engine/routing.js.map +1 -1
  78. package/dist/engine/snapshot.d.ts +107 -7
  79. package/dist/engine/state.d.ts +4 -2
  80. package/dist/engine/state.js.map +1 -1
  81. package/dist/engine/sync.d.ts +96 -0
  82. package/dist/engine/sync.js +199 -0
  83. package/dist/engine/sync.js.map +1 -0
  84. package/dist/engine/team.d.ts +1 -0
  85. package/dist/engine/team.js +12 -8
  86. package/dist/engine/team.js.map +1 -1
  87. package/dist/expert/_main.md +96 -67
  88. package/dist/expert/agents/_index.md +10 -7
  89. package/dist/expert/agents/core-agents.md +4 -2
  90. package/dist/expert/agents/fixer.md +15 -12
  91. package/dist/expert/cli/acceptance.md +96 -78
  92. package/dist/expert/cli/api.md +26 -14
  93. package/dist/expert/cli/autonomous-operation.md +17 -19
  94. package/dist/expert/cli/browser.md +24 -24
  95. package/dist/expert/cli/checks.md +18 -10
  96. package/dist/expert/cli/cli.md +48 -41
  97. package/dist/expert/cli/configuration-migrations.md +43 -0
  98. package/dist/expert/cli/configuration.md +24 -14
  99. package/dist/expert/cli/controls.md +23 -11
  100. package/dist/expert/cli/dashboard-details.md +28 -10
  101. package/dist/expert/cli/dashboard-layout.md +15 -15
  102. package/dist/expert/cli/dashboard.md +8 -10
  103. package/dist/expert/cli/environment.md +33 -0
  104. package/dist/expert/cli/execution.md +10 -8
  105. package/dist/expert/cli/git-persistence.md +27 -21
  106. package/dist/expert/cli/git-preflight.md +8 -4
  107. package/dist/expert/cli/git-synchronization.md +32 -6
  108. package/dist/expert/cli/harness-plugins.md +12 -12
  109. package/dist/expert/cli/harnesses.md +22 -16
  110. package/dist/expert/cli/initialization-ui.md +14 -14
  111. package/dist/expert/cli/initialization.md +31 -21
  112. package/dist/expert/cli/installation.md +6 -0
  113. package/dist/expert/cli/isolation.md +5 -3
  114. package/dist/expert/cli/releasing.md +17 -3
  115. package/dist/expert/cli/retries.md +10 -8
  116. package/dist/expert/cli/run.md +7 -5
  117. package/dist/expert/cli/start.md +21 -15
  118. package/dist/expert/cli/status-view.md +15 -17
  119. package/dist/expert/cli/synchronization-setup.md +24 -0
  120. package/dist/expert/cli/terminal.md +14 -16
  121. package/dist/expert/cli/tool-management.md +60 -0
  122. package/dist/expert/cli/tool-runtime.md +35 -0
  123. package/dist/expert/cli/tools-migration.md +50 -0
  124. package/dist/expert/cli/tools.md +73 -0
  125. package/dist/expert/cli/trace-configuration.md +9 -11
  126. package/dist/expert/cli/updates.md +7 -7
  127. package/dist/expert/cli/windows-installer.md +67 -0
  128. package/dist/expert/cli/workspace.md +2 -0
  129. package/dist/expert/coding-standards/_index.md +11 -0
  130. package/dist/expert/coding-standards/naming.md +30 -0
  131. package/dist/expert/coding-standards/shared-logic.md +27 -0
  132. package/dist/expert/concepts/automatic-repair.md +33 -0
  133. package/dist/expert/concepts/engine-and-project.md +7 -5
  134. package/dist/expert/dictionary.md +18 -18
  135. package/dist/harness/plugins/claude-code/index.js +4 -2
  136. package/dist/harness/plugins/claude-code/index.js.map +1 -1
  137. package/dist/harness/plugins/cline/index.js +1 -0
  138. package/dist/harness/plugins/cline/index.js.map +1 -1
  139. package/dist/harness/plugins/gemini/index.js +2 -0
  140. package/dist/harness/plugins/gemini/index.js.map +1 -1
  141. package/dist/harness/plugins/github-copilot/index.js +1 -0
  142. package/dist/harness/plugins/github-copilot/index.js.map +1 -1
  143. package/dist/harness/plugins/openai-codex/index.js +13 -9
  144. package/dist/harness/plugins/openai-codex/index.js.map +1 -1
  145. package/dist/harness/plugins/opencode/index.js +1 -0
  146. package/dist/harness/plugins/opencode/index.js.map +1 -1
  147. package/dist/harness/plugins/qwen-code/index.js +1 -0
  148. package/dist/harness/plugins/qwen-code/index.js.map +1 -1
  149. package/dist/harness/types.d.ts +16 -5
  150. package/dist/project/config.d.ts +135 -48
  151. package/dist/project/config.js +331 -119
  152. package/dist/project/config.js.map +1 -1
  153. package/dist/project/configuration.d.ts +43 -0
  154. package/dist/project/configuration.js +165 -0
  155. package/dist/project/configuration.js.map +1 -0
  156. package/dist/project/defaults.d.ts +4 -2
  157. package/dist/project/defaults.js +10 -5
  158. package/dist/project/defaults.js.map +1 -1
  159. package/dist/project/git.d.ts +28 -0
  160. package/dist/project/git.js +81 -0
  161. package/dist/project/git.js.map +1 -1
  162. package/dist/project/migrations.d.ts +96 -0
  163. package/dist/project/migrations.js +301 -0
  164. package/dist/project/migrations.js.map +1 -0
  165. package/dist/project/publication.d.ts +57 -0
  166. package/dist/project/publication.js +117 -0
  167. package/dist/project/publication.js.map +1 -0
  168. package/dist/project/readiness.d.ts +14 -5
  169. package/dist/project/readiness.js +23 -22
  170. package/dist/project/readiness.js.map +1 -1
  171. package/dist/project/setup.js +2 -0
  172. package/dist/project/setup.js.map +1 -1
  173. package/dist/service/actions.d.ts +12 -4
  174. package/dist/service/actions.js +32 -2
  175. package/dist/service/actions.js.map +1 -1
  176. package/dist/service/control.d.ts +1 -1
  177. package/dist/service/control.js +1 -1
  178. package/dist/service/host.d.ts +5 -2
  179. package/dist/service/host.js +26 -16
  180. package/dist/service/host.js.map +1 -1
  181. package/dist/service/http.js +7 -2
  182. package/dist/service/http.js.map +1 -1
  183. package/dist/service/launcher.d.ts +2 -1
  184. package/dist/service/launcher.js +1 -0
  185. package/dist/service/launcher.js.map +1 -1
  186. package/dist/service/records.d.ts +3 -0
  187. package/dist/service/records.js +1 -1
  188. package/dist/service/records.js.map +1 -1
  189. package/dist/service/spa/app.css +39 -0
  190. package/dist/service/spa/app.js +242 -39
  191. package/dist/service/worker.js +1 -0
  192. package/dist/service/worker.js.map +1 -1
  193. package/dist/tools/environment.d.ts +122 -0
  194. package/dist/tools/environment.js +199 -0
  195. package/dist/tools/environment.js.map +1 -0
  196. package/dist/tools/model.d.ts +180 -0
  197. package/dist/tools/model.js +111 -0
  198. package/dist/tools/model.js.map +1 -0
  199. package/dist/tools/normalize.d.ts +51 -0
  200. package/dist/tools/normalize.js +321 -0
  201. package/dist/tools/normalize.js.map +1 -0
  202. package/dist/tools/operations.d.ts +198 -0
  203. package/dist/tools/operations.js +378 -0
  204. package/dist/tools/operations.js.map +1 -0
  205. package/dist/tools/readiness.d.ts +72 -0
  206. package/dist/tools/readiness.js +160 -0
  207. package/dist/tools/readiness.js.map +1 -0
  208. package/dist/ui/dashboard.d.ts +3 -0
  209. package/dist/ui/dashboard.js +173 -2
  210. package/dist/ui/dashboard.js.map +1 -1
  211. package/dist/ui/details.d.ts +35 -1
  212. package/dist/ui/details.js +103 -2
  213. package/dist/ui/details.js.map +1 -1
  214. package/dist/ui/secret.d.ts +8 -0
  215. package/dist/ui/secret.js +41 -0
  216. package/dist/ui/secret.js.map +1 -0
  217. package/dist/ui/status-view.d.ts +1 -0
  218. package/dist/ui/status-view.js +33 -8
  219. package/dist/ui/status-view.js.map +1 -1
  220. package/dist/ui/tool-forms.d.ts +24 -0
  221. package/dist/ui/tool-forms.js +133 -0
  222. package/dist/ui/tool-forms.js.map +1 -0
  223. package/package.json +1 -1
@@ -6,10 +6,11 @@ import { PtbkError, errorMessage } from '../common/errors.js';
6
6
  import { ensureDirectory, pathExists } from '../common/fs.js';
7
7
  import { shortHash, shortId } from '../common/ids.js';
8
8
  import { isProcessAlive, terminateProcessTree, which } from '../common/process.js';
9
- import { setting } from '../project/config.js';
9
+ import { MIGRATED_BROWSER_NAME } from '../project/migrations.js';
10
+ import { describeProfileSource } from '../tools/model.js';
10
11
  import { createRedactor } from '../common/redact.js';
11
12
  import { promptbookAppData } from '../common/app-data.js';
12
- import { BrowserRecorder, cleanStaleRecordings } from './recording.js';
13
+ import { BrowserRecorder } from './recording.js';
13
14
  /**
14
15
  * Maximal page text returned to an agent.
15
16
  */
@@ -27,45 +28,39 @@ const LAUNCH_ATTEMPTS = 3;
27
28
  */
28
29
  const LAUNCH_RETRY_MS = 3000;
29
30
  /**
30
- * Candidate Chrome-compatible executables per platform.
31
+ * Candidate executables of each browser adapter per platform (in preference order).
31
32
  */
32
- function chromeCandidates() {
33
- if (process.platform === 'darwin') {
34
- return ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'];
35
- }
36
- if (process.platform === 'win32') {
37
- const roots = [process.env['PROGRAMFILES'], process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter(Boolean);
38
- return roots.flatMap((root) => [join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'), join(root, 'Microsoft', 'Edge', 'Application', 'msedge.exe'), join(root, 'Chromium', 'Application', 'chrome.exe')]);
39
- }
40
- return [];
33
+ function browserCandidates(variant) {
34
+ const chrome = { mac: ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'], win: [join('Google', 'Chrome', 'Application', 'chrome.exe')], names: ['google-chrome', 'google-chrome-stable', 'chrome'] };
35
+ const chromium = { mac: ['/Applications/Chromium.app/Contents/MacOS/Chromium'], win: [join('Chromium', 'Application', 'chrome.exe')], names: ['chromium', 'chromium-browser'] };
36
+ const edge = { mac: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'], win: [join('Microsoft', 'Edge', 'Application', 'msedge.exe')], names: ['microsoft-edge', 'microsoft-edge-stable', 'msedge'] };
37
+ const brave = { mac: ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'], win: [], names: [] };
38
+ // CHROME prefers Google Chrome and accepts another installed Chrome-compatible browser when it is absent.
39
+ const groups = variant === 'CHROMIUM' ? [chromium] : variant === 'EDGE' ? [edge] : [chrome, chromium, edge, brave];
40
+ const roots = [process.env['PROGRAMFILES'], process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter(Boolean);
41
+ const paths = groups.flatMap((group) => (process.platform === 'darwin' ? group.mac : process.platform === 'win32' ? roots.flatMap((root) => group.win.map((relative) => join(root, relative))) : []));
42
+ return { paths, names: groups.flatMap((group) => group.names) };
41
43
  }
42
44
  /**
43
- * Locates a Chrome-compatible browser (configuration, `CHROME_PATH`, standard locations, PATH).
45
+ * Locates the executable of a browser tool (its explicit executable, `CHROME_PATH`, standard locations, PATH).
44
46
  *
45
47
  * @private project browser
46
48
  */
47
- export async function findBrowserExecutable(settings, env = process.env) {
48
- const configured = setting(settings, 'browser.executable') ?? env.CHROME_PATH;
49
+ export async function findBrowserExecutable(tool, env = process.env) {
50
+ const configured = tool.executable ?? env.CHROME_PATH;
49
51
  if (configured)
50
52
  return (await pathExists(configured)) ? configured : await which(configured, env);
51
- for (const candidate of chromeCandidates())
53
+ const candidates = browserCandidates(tool.variant);
54
+ for (const candidate of candidates.paths)
52
55
  if (await pathExists(candidate))
53
56
  return candidate;
54
- for (const name of ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome', 'msedge']) {
57
+ for (const name of candidates.names) {
55
58
  const found = await which(name, env);
56
59
  if (found)
57
60
  return found;
58
61
  }
59
62
  return undefined;
60
63
  }
61
- /**
62
- * Classifies a `browser.profile` value: `global`, `isolated`, `user`, or a custom profile path.
63
- *
64
- * @private project browser
65
- */
66
- export function browserProfileSource(value) {
67
- return value === 'global' || value === 'isolated' || value === 'user' ? value : 'custom';
68
- }
69
64
  export { promptbookAppData };
70
65
  /**
71
66
  * The dedicated Promptbook-managed browser profile shared by this user's projects (not the user's own Chrome profile).
@@ -88,40 +83,30 @@ export function userChromeProfile(env = process.env) {
88
83
  return join(homedir(), '.config', 'google-chrome');
89
84
  }
90
85
  /**
91
- * Resolves the profile directory of the selected source: the managed global profile, the isolated project profile in
92
- * `.ptbk/`, the user's Chrome profile, or a custom path (relative paths resolve against the project).
86
+ * Resolves the profile directory of a browser tool at runtime: the managed global profile (per OS user), an isolated
87
+ * project profile in `.ptbk/` (separate per instance), the user's Chrome profile, or a custom path (relative paths
88
+ * resolve against the project).
93
89
  *
94
90
  * @private project browser
95
91
  */
96
- export function browserProfileDirectory(project, settings, env = process.env) {
97
- const profile = setting(settings, 'browser.profile');
98
- switch (browserProfileSource(profile)) {
99
- case 'global':
100
- return globalBrowserProfile(env);
101
- case 'isolated':
102
- return project.runtime.browserProfile;
103
- case 'user':
104
- return userChromeProfile(env);
105
- case 'custom':
106
- return resolve(project.root, profile);
92
+ export function browserProfileDirectory(project, tool, env = process.env) {
93
+ switch (tool.profile.source) {
94
+ case 'MANAGED_GLOBAL':
95
+ return tool.profile.path ? resolve(promptbookAppData(env), tool.profile.path) : globalBrowserProfile(env);
96
+ case 'PROJECT':
97
+ // The first (migrated) browser keeps the historical location so existing sessions survive.
98
+ return tool.profile.path ? resolve(project.root, tool.profile.path) : tool.name === MIGRATED_BROWSER_NAME ? project.runtime.browserProfile : join(project.runtime.root, 'browser-profiles', tool.name);
99
+ case 'USER_EXISTING':
100
+ return tool.profile.path ? resolve(tool.profile.path) : userChromeProfile(env);
101
+ case 'CUSTOM':
102
+ return resolve(project.root, tool.profile.path ?? '.');
107
103
  }
108
104
  }
109
105
  /**
110
- * Human description of a `browser.profile` value.
111
- *
112
- * @private project browser
106
+ * Whether a profile source is shared beyond this project (claimed per profile across projects, released after tasks).
113
107
  */
114
- export function describeBrowserProfile(value) {
115
- switch (browserProfileSource(value)) {
116
- case 'global':
117
- return 'dedicated Promptbook profile shared by your projects';
118
- case 'isolated':
119
- return 'isolated project profile';
120
- case 'user':
121
- return 'your Chrome profile';
122
- case 'custom':
123
- return `custom profile ${value}`;
124
- }
108
+ function isSharedSource(source) {
109
+ return source !== 'PROJECT';
125
110
  }
126
111
  /**
127
112
  * Cross-project ownership of a shared profile: one project's automation at a time (`<app data>/locks/`).
@@ -258,13 +243,16 @@ class CdpConnection {
258
243
  }
259
244
  }
260
245
  /**
261
- * The project's single shared browser instance (one profile, one automation owner at a time).
246
+ * One named browser tool of the project: one profile and one automation owner at a time, shared by the project's
247
+ * agents. A launched process keeps the configuration it was acquired with (retained use) until the task that uses
248
+ * it releases it; later selections see the new saved configuration.
262
249
  *
263
250
  * @private project browser
264
251
  */
265
252
  export class ProjectBrowser {
266
253
  project;
267
- settings;
254
+ name;
255
+ target;
268
256
  env;
269
257
  onChange;
270
258
  child;
@@ -273,6 +261,8 @@ export class ProjectBrowser {
273
261
  connection;
274
262
  sessionId;
275
263
  note;
264
+ /** Configuration the running process was launched with (retained while a task uses it). */
265
+ launchedWith;
276
266
  /** A launch in progress (concurrent automatic/manual starts share it: never two owners). */
277
267
  launching;
278
268
  /** The current task used the browser (a restart waits for the task boundary). */
@@ -283,15 +273,34 @@ export class ProjectBrowser {
283
273
  health = { state: 'not-started', ownership: { state: 'unknown' }, recovery: { attempts: 0, state: 'idle' } };
284
274
  /** Task-linked recording of browser activity (`traces/<task-file-basename>/video/`). */
285
275
  recorder;
286
- constructor(project, settings, env,
276
+ constructor(project,
277
+ /** Name of the browser tool this instance serves. */
278
+ name, target, env,
287
279
  /** Publishes health changes to every view. */
288
- onChange = () => undefined) {
280
+ onChange = () => undefined, recordingEnabled = () => true) {
289
281
  this.project = project;
290
- this.settings = settings;
282
+ this.name = name;
283
+ this.target = target;
291
284
  this.env = env;
292
285
  this.onChange = onChange;
293
- this.recorder = new BrowserRecorder(project, () => setting(this.settings(), 'traces.recordVideo'), env);
294
- void cleanStaleRecordings(project);
286
+ this.recorder = new BrowserRecorder(project, recordingEnabled, env);
287
+ }
288
+ /** Configuration of the next use: the retained one while a task holds the process, otherwise the saved one. */
289
+ config() {
290
+ if (this.child && this.usedByTask && this.launchedWith)
291
+ return this.launchedWith;
292
+ return this.target()?.tool;
293
+ }
294
+ /** Whether a task currently holds this browser. */
295
+ get inUse() {
296
+ return this.usedByTask && Boolean(this.child);
297
+ }
298
+ /** Whether the running process still uses a configuration that was changed, disabled or removed meanwhile. */
299
+ get retained() {
300
+ if (!this.child || !this.launchedWith)
301
+ return false;
302
+ const saved = this.target()?.tool;
303
+ return !saved || !saved.isEnabled || JSON.stringify(saved) !== JSON.stringify(this.launchedWith);
295
304
  }
296
305
  /** Updates the observed health and notifies the views. */
297
306
  setHealth(changes) {
@@ -309,19 +318,30 @@ export class ProjectBrowser {
309
318
  * ownership, last failure and recovery. A profile path is diagnostic data, never permission to read its contents.
310
319
  */
311
320
  inspect() {
312
- const settings = this.settings();
313
- const profile = setting(settings, 'browser.profile');
314
- const profilePath = browserProfileDirectory(this.project, settings, this.env);
315
- const source = browserProfileSource(profile);
316
- const overridden = ['cli', 'runtime'].includes(settings.sources['browser.profile'] ?? 'default');
317
- const active = this.health.profilePath && this.child && this.health.profilePath !== profilePath ? this.health.profilePath : undefined;
321
+ const target = this.target();
322
+ const saved = target?.tool;
323
+ const effective = this.config() ?? saved;
324
+ const profilePath = effective ? browserProfileDirectory(this.project, effective, this.env) : '';
325
+ const savedPath = saved ? browserProfileDirectory(this.project, saved, this.env) : undefined;
326
+ const retained = this.retained;
318
327
  return {
319
- source,
320
- sourceLabel: describeBrowserProfile(profile),
321
- profilePath: active ?? profilePath,
322
- savedProfile: overridden ? `overridden for this process (${settings.sources['browser.profile']}); the saved default stays in ptbk.json` : undefined,
323
- pending: active ? `The selected profile changes to ${profilePath} at the next task boundary (after the browser restarts).` : this.pendingRestart ? 'A browser restart waits for the current task to release the browser.' : undefined,
324
- mode: { requested: setting(settings, 'browser.mode') === 'headful' ? 'headful' : 'headless', actual: this.child ? this.health.actualMode : undefined, note: this.note },
328
+ name: this.name,
329
+ variant: effective?.variant ?? 'CHROME',
330
+ isEnabled: saved?.isEnabled ?? false,
331
+ source: effective?.profile.source ?? 'MANAGED_GLOBAL',
332
+ sourceLabel: effective ? describeProfileSource(effective.profile.source, effective.profile.path) : 'not configured',
333
+ profilePath,
334
+ savedProfile: target?.overridden ? 'overridden for this process (command-line flags); the saved tool stays in ptbk.json' : undefined,
335
+ pending: retained
336
+ ? saved
337
+ ? `The current task keeps using the configuration it acquired; the saved change${savedPath && savedPath !== profilePath ? ` (profile ${savedPath})` : ''} applies after it releases the browser.`
338
+ : 'This tool was disabled or removed; the current task finishes with it, no new selection uses it.'
339
+ : this.pendingRestart
340
+ ? 'A browser restart waits for the current task to release the browser.'
341
+ : undefined,
342
+ retained: retained || undefined,
343
+ inUse: this.inUse || undefined,
344
+ mode: { requested: effective?.isHeadless === false ? 'headful' : 'headless', actual: this.child ? this.health.actualMode : undefined, note: this.note },
325
345
  executable: this.health.executable,
326
346
  state: this.health.state,
327
347
  pid: this.child?.pid,
@@ -342,14 +362,16 @@ export class ProjectBrowser {
342
362
  * connection when the browser runs.
343
363
  */
344
364
  async recheck() {
345
- const settings = this.settings();
365
+ const tool = this.config();
366
+ if (!tool)
367
+ return this.acted('recheck', 'failed', `The browser tool "${this.name}" is no longer configured.`);
346
368
  const findings = [];
347
- const executable = await findBrowserExecutable(settings, this.env);
348
- findings.push(executable ? `browser ${executable}` : 'no Chrome-compatible browser installed');
349
- const profile = browserProfileDirectory(this.project, settings, this.env);
350
- const source = browserProfileSource(setting(settings, 'browser.profile'));
369
+ const executable = await findBrowserExecutable(tool, this.env);
370
+ findings.push(executable ? `browser ${executable}` : `no ${tool.variant === 'CHROME' ? 'Chrome-compatible' : tool.variant} browser installed`);
371
+ const profile = browserProfileDirectory(this.project, tool, this.env);
372
+ const source = tool.profile.source;
351
373
  const exists = await pathExists(profile);
352
- findings.push(exists ? `profile ${profile}` : source === 'user' || source === 'custom' ? `profile ${profile} does not exist` : `profile ${profile} is created on first use`);
374
+ findings.push(exists ? `profile ${profile}` : source === 'USER_EXISTING' || source === 'CUSTOM' ? `profile ${profile} does not exist` : `profile ${profile} is created on first use`);
353
375
  const holder = this.child ? undefined : await profileOwner(profile);
354
376
  const ownership = this.child ? { state: 'owned' } : holder ? { state: 'busy', holder: `browser PID ${holder}` } : { state: 'free' };
355
377
  if (holder)
@@ -370,13 +392,13 @@ export class ProjectBrowser {
370
392
  else if (state === 'running')
371
393
  state = 'stopped';
372
394
  this.setHealth({ state, executable: executable ?? undefined, ownership });
373
- const healthy = Boolean(executable) && (exists || source === 'global' || source === 'isolated') && !holder && state !== 'failed';
395
+ const healthy = Boolean(executable) && (exists || source === 'MANAGED_GLOBAL' || source === 'PROJECT') && !holder && state !== 'failed';
374
396
  return this.acted('recheck', healthy ? 'succeeded' : 'failed', `${healthy ? 'Browser diagnostics OK' : 'Browser problem found'}: ${findings.join('; ')}.`);
375
397
  }
376
398
  /** Retries browser startup now (sharing any launch already in progress); a shared profile is released again when idle. */
377
399
  async retry() {
378
400
  if (this.connection && this.sessionId)
379
- return this.acted('retry', 'succeeded', 'The project browser is already running.');
401
+ return this.acted('retry', 'succeeded', `The browser "${this.name}" is already running.`);
380
402
  this.setHealth({ recovery: { ...this.health.recovery, attempts: 0, state: 'retrying' } });
381
403
  try {
382
404
  await this.ensure();
@@ -386,7 +408,7 @@ export class ProjectBrowser {
386
408
  }
387
409
  if (!this.usedByTask)
388
410
  this.releaseShared();
389
- return this.acted('retry', 'succeeded', this.child ? 'The project browser started.' : 'The project browser started successfully and was released again (shared profile, no task needs it now).');
411
+ return this.acted('retry', 'succeeded', this.child ? `The browser "${this.name}" started.` : `The browser "${this.name}" started successfully and was released again (shared profile, no task needs it now).`);
390
412
  }
391
413
  /**
392
414
  * Restarts only this project's own browser process, preserving the selected profile and its sessions. While a task
@@ -404,22 +426,28 @@ export class ProjectBrowser {
404
426
  this.pendingRestart = false;
405
427
  const wasRunning = Boolean(this.child);
406
428
  this.close();
407
- const shared = browserProfileSource(setting(this.settings(), 'browser.profile')) !== 'isolated';
429
+ const shared = isSharedSource(this.config()?.profile.source ?? 'MANAGED_GLOBAL');
408
430
  if (shared)
409
- return this.acted('restart', 'succeeded', wasRunning ? 'Closed this project’s browser; the next use starts it again with the same profile.' : 'The browser was not running; the next use starts it with the same profile.');
431
+ return this.acted('restart', 'succeeded', wasRunning ? `Closed the browser "${this.name}"; the next use starts it again with the same profile.` : 'The browser was not running; the next use starts it with the same profile.');
410
432
  try {
411
433
  await this.ensure();
412
- return this.acted('restart', 'succeeded', 'Restarted the project browser with the same profile.');
434
+ return this.acted('restart', 'succeeded', `Restarted the browser "${this.name}" with the same profile.`);
413
435
  }
414
436
  catch (error) {
415
437
  return this.acted('restart', 'failed', `The browser did not start again: ${errorMessage(error)}`);
416
438
  }
417
439
  }
418
- /** After a task: performs a deferred restart and releases a shared profile. */
440
+ /**
441
+ * After a task: performs a deferred restart, ends a retained use of a changed/disabled/removed configuration and
442
+ * releases a shared profile.
443
+ */
419
444
  async taskFinished() {
445
+ const retained = this.retained;
420
446
  this.usedByTask = false;
421
447
  if (this.pendingRestart)
422
448
  await this.restartNow().catch(() => undefined);
449
+ if (retained)
450
+ this.close();
423
451
  this.releaseShared();
424
452
  }
425
453
  /** Handles a lost connection: stops only this project's browser process; the next use relaunches it. */
@@ -432,13 +460,9 @@ export class ProjectBrowser {
432
460
  terminateProcessTree(this.child);
433
461
  this.child = undefined;
434
462
  void this.releaseProfile();
435
- this.recordFailure(new PtbkError('The connection to the project browser was lost.', { kind: 'technical', reason }));
463
+ this.recordFailure(new PtbkError(`The connection to the browser "${this.name}" was lost.`, { kind: 'technical', reason }));
436
464
  this.setHealth({ state: 'failed', recovery: { ...this.health.recovery, state: 'retrying' } });
437
465
  }
438
- /** Associates subsequent browser use with a task attempt for recording. */
439
- async useFor(trace, attempt) {
440
- await this.recorder.attach(trace, attempt);
441
- }
442
466
  /** Unsubscribes the page-transition listener of the current connection. */
443
467
  navigationListener;
444
468
  /** Ensures the browser runs and recording of the attached task has begun. */
@@ -501,21 +525,26 @@ export class ProjectBrowser {
501
525
  }
502
526
  /** One launch attempt of the configured browser and profile. */
503
527
  async launchOnce() {
504
- const settings = this.settings();
505
- const executable = await findBrowserExecutable(settings, this.env);
528
+ const tool = this.target()?.tool;
529
+ if (!tool)
530
+ throw new PtbkError(`The browser tool "${this.name}" is not configured.`, { kind: 'configuration', next: ['Add a browser with `ptbk tool add BROWSER …`.'] });
531
+ if (!tool.isEnabled)
532
+ throw new PtbkError(`The browser tool "${this.name}" is disabled.`, { kind: 'configuration', next: [`Enable it: ptbk tool enable ${this.name}`] });
533
+ const executable = await findBrowserExecutable(tool, this.env);
506
534
  if (!executable)
507
- throw new PtbkError('No Chrome-compatible browser is installed.', { kind: 'setup', next: ['Install Google Chrome or Chromium, or set ptbk.json browser.executable.'] });
508
- const source = browserProfileSource(setting(settings, 'browser.profile'));
509
- const profile = browserProfileDirectory(this.project, settings, this.env);
535
+ throw new PtbkError(`No ${tool.variant === 'CHROME' ? 'Chrome-compatible' : tool.variant} browser is installed for "${this.name}".`, { kind: 'setup', next: [`Install Google Chrome or Chromium, or set its executable: ptbk tool modify ${this.name} --executable <path>`] });
536
+ const source = tool.profile.source;
537
+ const profile = browserProfileDirectory(this.project, tool, this.env);
510
538
  this.health = { ...this.health, executable, profilePath: profile };
511
- if (source === 'user' || source === 'custom') {
539
+ if (source === 'USER_EXISTING' || source === 'CUSTOM') {
512
540
  // An existing profile is reused as it is; it is never created, copied or migrated implicitly.
513
541
  if (!(await pathExists(profile)))
514
- throw new PtbkError(`The selected browser profile ${profile} does not exist.`, { kind: 'configuration', next: ['Choose another profile in ptbk.json `browser.profile` (global, isolated, user or a path), or pass --browser-profile.'] });
542
+ throw new PtbkError(`The selected browser profile ${profile} does not exist.`, { kind: 'configuration', next: [`Choose another profile: ptbk tool modify ${this.name} --profile-source MANAGED_GLOBAL|PROJECT|USER_EXISTING|CUSTOM [--profile-path <path>]`] });
515
543
  }
516
544
  else
517
545
  await ensureDirectory(profile);
518
- if (source !== 'isolated') {
546
+ this.launchedWith = tool;
547
+ if (isSharedSource(source)) {
519
548
  let claim = await claimProfile(profile, this.project, this.env);
520
549
  for (let attempt = 0; 'owner' in claim && attempt < 10; attempt++) {
521
550
  await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -542,10 +571,10 @@ export class ProjectBrowser {
542
571
  this.setHealth({ ownership: { state: 'busy', holder: `browser PID ${owner}` } });
543
572
  throw new PtbkError(`The browser profile ${profile} is in use by another browser (PID ${owner}).`, {
544
573
  kind: 'conflict',
545
- next: ['Close that browser to hand the profile over, or choose another profile (ptbk.json browser.profile).'],
574
+ next: [`Close that browser to hand the profile over, or choose another profile (ptbk tool modify ${this.name} --profile-source …).`],
546
575
  });
547
576
  }
548
- let headless = setting(settings, 'browser.mode') !== 'headful';
577
+ let headless = tool.isHeadless;
549
578
  if (!headless && !hasDisplay(this.env)) {
550
579
  headless = true;
551
580
  this.note = 'No display is available on this host, so the browser runs headless (no window was shown).';
@@ -711,17 +740,18 @@ export class ProjectBrowser {
711
740
  releaseShared() {
712
741
  if (!this.child && !this.releaseClaim)
713
742
  return;
714
- if (browserProfileSource(setting(this.settings(), 'browser.profile')) === 'isolated')
743
+ if (!isSharedSource(this.config()?.profile.source ?? 'MANAGED_GLOBAL'))
715
744
  return;
716
745
  this.close();
717
746
  }
718
747
  }
719
748
  /**
720
- * Browser tools exposed to agents through the ptbk MCP bridge.
749
+ * Browser tools exposed to agents through the ptbk MCP bridge. Every call resolves the live inventory: a browser
750
+ * added during the task is selectable at the next call; `browser` names a specific tool (account/profile).
721
751
  *
722
752
  * @private project browser
723
753
  */
724
- export function browserTools(browser, chats, agent, onUserChat) {
754
+ export function browserTools(fleet, chats, agent, onUserChat, describe) {
725
755
  const run = async (action) => {
726
756
  try {
727
757
  return { text: await action() };
@@ -730,50 +760,64 @@ export function browserTools(browser, chats, agent, onUserChat) {
730
760
  return { text: errorMessage(error), isError: true };
731
761
  }
732
762
  };
763
+ const name = (input) => (typeof input.browser === 'string' && input.browser.trim() ? input.browser.trim() : undefined);
764
+ const browserProperty = { browser: { type: 'string', description: 'Name of the browser tool to use (see browser_list); defaults to the browser this task already uses, else the first enabled one.' } };
733
765
  return [
766
+ {
767
+ name: 'browser_list',
768
+ description: "List the project's enabled browser tools (names, descriptions, profile kinds). Choose the one whose account/profile fits the task; never act from a different account merely because it is logged in.",
769
+ inputSchema: { type: 'object', properties: {} },
770
+ handler: () => run(async () => {
771
+ const browsers = describe();
772
+ if (!browsers.length)
773
+ return 'No browser tool is enabled for this project.';
774
+ return browsers.map((browser) => `- ${browser.name}${browser.description ? `: ${browser.description}` : ''} (${browser.profile}, ${browser.isHeadless ? 'headless' : 'visible window'})`).join('\n');
775
+ }),
776
+ },
734
777
  {
735
778
  name: 'browser_open',
736
- description: "Open a URL in the project's shared browser (persistent logged-in sessions) and return the page text and interactive elements.",
737
- inputSchema: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },
738
- handler: (input) => run(() => browser.open(String(input.url ?? ''))),
779
+ description: 'Open a URL in a project browser (persistent logged-in sessions of its profile) and return the page text and interactive elements.',
780
+ inputSchema: { type: 'object', properties: { url: { type: 'string' }, ...browserProperty }, required: ['url'] },
781
+ handler: (input) => run(async () => (await fleet.acquire(name(input))).open(String(input.url ?? ''))),
739
782
  },
740
783
  {
741
784
  name: 'browser_read',
742
785
  description: 'Return the text and numbered interactive elements of the current page.',
743
- inputSchema: { type: 'object', properties: {} },
744
- handler: () => run(() => browser.read()),
786
+ inputSchema: { type: 'object', properties: { ...browserProperty } },
787
+ handler: (input) => run(async () => (await fleet.acquire(name(input))).read()),
745
788
  },
746
789
  {
747
790
  name: 'browser_click',
748
791
  description: 'Click an interactive element by its number from browser_read.',
749
- inputSchema: { type: 'object', properties: { ref: { type: 'number' } }, required: ['ref'] },
750
- handler: (input) => run(() => browser.click(Number(input.ref))),
792
+ inputSchema: { type: 'object', properties: { ref: { type: 'number' }, ...browserProperty }, required: ['ref'] },
793
+ handler: (input) => run(async () => (await fleet.acquire(name(input))).click(Number(input.ref))),
751
794
  },
752
795
  {
753
796
  name: 'browser_type',
754
797
  description: 'Type text into an input by its number from browser_read; optionally submit the form.',
755
- inputSchema: { type: 'object', properties: { ref: { type: 'number' }, text: { type: 'string' }, submit: { type: 'boolean' } }, required: ['ref', 'text'] },
756
- handler: (input) => run(() => browser.type(Number(input.ref), String(input.text ?? ''), input.submit === true)),
798
+ inputSchema: { type: 'object', properties: { ref: { type: 'number' }, text: { type: 'string' }, submit: { type: 'boolean' }, ...browserProperty }, required: ['ref', 'text'] },
799
+ handler: (input) => run(async () => (await fleet.acquire(name(input))).type(Number(input.ref), String(input.text ?? ''), input.submit === true)),
757
800
  },
758
801
  {
759
802
  name: 'browser_screenshot',
760
803
  description: 'Save a screenshot of the current page and return its file path.',
761
- inputSchema: { type: 'object', properties: {} },
762
- handler: () => run(() => browser.screenshot()),
804
+ inputSchema: { type: 'object', properties: { ...browserProperty } },
805
+ handler: (input) => run(async () => (await fleet.acquire(name(input))).screenshot()),
763
806
  },
764
807
  {
765
808
  name: 'browser_request_user_action',
766
- description: 'Only when a website genuinely requires a human-only step (sign-in, two-factor authentication, consent) that the existing session cannot satisfy: ask the human to act in the shared browser. Then end your turn with status waiting-for-user.',
767
- inputSchema: { type: 'object', properties: { reason: { type: 'string' } }, required: ['reason'] },
809
+ description: 'Only when a website genuinely requires a human-only step (sign-in, two-factor authentication, consent) that the existing session cannot satisfy: ask the human to act in the project browser. Then end your turn with status waiting-for-user.',
810
+ inputSchema: { type: 'object', properties: { reason: { type: 'string' }, ...browserProperty }, required: ['reason'] },
768
811
  handler: (input) => run(async () => {
769
812
  if (!chats)
770
813
  throw new PtbkError('The user is not available in this run; report blocked with the reason instead.', { kind: 'unsupported' });
814
+ const browser = await fleet.acquire(name(input));
771
815
  const note = await browser.bringToFront();
772
816
  const chat = await chats.askUser({
773
817
  agent,
774
- reason: String(input.reason ?? 'A website needs your action in the project browser.'),
775
- attempted: 'I used the existing session of the shared project browser; the website still requires this human-only step.',
776
- question: `Please complete the action in the project browser${note ? ` (${note})` : ''}, then confirm here.`,
818
+ reason: String(input.reason ?? `A website needs your action in the browser "${browser.name}".`),
819
+ attempted: `I used the existing session of the project browser "${browser.name}"; the website still requires this human-only step.`,
820
+ question: `Please complete the action in the browser "${browser.name}"${note ? ` (${note})` : ''}, then confirm here.`,
777
821
  choices: ['Done', 'I cannot do it'],
778
822
  });
779
823
  onUserChat(chat);