reslop 0.3.1 → 0.3.2

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 (65) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +21 -3
  3. package/lib/agents/acp.js +309 -0
  4. package/lib/agents/agents.js +163 -436
  5. package/lib/agents/allow.js +141 -52
  6. package/lib/agents/allow.json +16 -14
  7. package/lib/agents/models.js +250 -383
  8. package/lib/agents/registry.js +437 -0
  9. package/lib/agents/run.js +234 -0
  10. package/lib/agents/transcript.js +143 -0
  11. package/lib/cli.js +2 -1
  12. package/lib/common/process.js +26 -1
  13. package/lib/dashboard/git.js +11 -1
  14. package/lib/dashboard/reports.js +80 -0
  15. package/lib/dashboard/watch.js +43 -8
  16. package/lib/dashboard/workspace.js +19 -20
  17. package/lib/dashboard/worktrees.js +145 -0
  18. package/lib/git/dir.js +50 -0
  19. package/lib/git/linked.js +152 -0
  20. package/lib/git/local.js +179 -0
  21. package/lib/git/status.js +126 -0
  22. package/lib/git/worktree.js +113 -36
  23. package/lib/input/actions.js +83 -5
  24. package/lib/input/keys.js +30 -1
  25. package/lib/input/line-edit.js +5 -1
  26. package/lib/render/agents.js +50 -24
  27. package/lib/render/branches.js +55 -27
  28. package/lib/render/field.js +19 -5
  29. package/lib/render/footer.js +14 -6
  30. package/lib/render/frame.js +16 -2
  31. package/lib/render/menu.js +103 -6
  32. package/lib/render/name-edit.js +31 -0
  33. package/lib/render/repos.js +52 -8
  34. package/lib/render/status.js +22 -4
  35. package/lib/report/run.js +16 -23
  36. package/lib/review/agent-instructions.md +2 -2
  37. package/lib/runs/process.js +3 -3
  38. package/lib/runs/runs.js +27 -4
  39. package/lib/session/agents/agents.js +220 -205
  40. package/lib/session/agents/away.js +48 -0
  41. package/lib/session/agents/job.js +23 -86
  42. package/lib/session/agents/jobs.js +14 -27
  43. package/lib/session/agents/options.js +45 -101
  44. package/lib/session/agents/plans.js +53 -0
  45. package/lib/session/agents/view.js +34 -0
  46. package/lib/session/branches.js +18 -1
  47. package/lib/session/compose.js +13 -5
  48. package/lib/session/dispatch.js +43 -4
  49. package/lib/session/editor-pointer.js +98 -0
  50. package/lib/session/files-pane.js +3 -0
  51. package/lib/session/lifecycle.js +67 -10
  52. package/lib/session/navigation.js +5 -0
  53. package/lib/session/npm/history.js +2 -1
  54. package/lib/session/npm/npm.js +89 -12
  55. package/lib/session/ops.js +3 -0
  56. package/lib/session/pointer.js +304 -46
  57. package/lib/session/session.js +15 -2
  58. package/lib/session/terminal.js +2 -2
  59. package/lib/session/text-menu.js +50 -0
  60. package/lib/session/view.js +19 -2
  61. package/lib/session/workspace.js +179 -38
  62. package/lib/session/worktrees.js +280 -0
  63. package/lib/term/themes/dark.json +4 -0
  64. package/lib/term/themes/light.json +4 -0
  65. package/package.json +2 -2
@@ -2,164 +2,109 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const { jsonParse } = require('metautil');
6
- const { IS_WIN, runProc, spawnBase } = require('../common/process.js');
7
- const { trimText } = require('../common/utilities.js');
5
+ const { IS_WIN, npmBin, npmOpts, spawnBase } = require('../common/process.js');
8
6
  const { commandEnv, PipeProcess } = require('../runs/process.js');
9
- const { ensurePlanAllows, PLAN_ALLOWS } = require('./allow.js');
7
+ const { trimText } = require('../common/utilities.js');
8
+ const { COMMANDS } = require('./allow.js');
10
9
  const models = require('./models.js');
11
- const { parseNameList, parseCursorModels, parseCursorWide } = models;
12
- const { parseJsonModels, catalogOf, emptyChoice, contextMenu } = models;
13
- const { parseJsonEfforts, parseHelpModels, optionValues } = models;
10
+ const { choiceOf } = models;
11
+ const { startRun, probeSession } = require('./run.js');
12
+ const registry = require('./registry.js');
13
+ const { installedRegistryAgent } = registry;
14
+ const { resolveGitDirs } = require('../git/dir.js');
14
15
 
15
- const LIST_MS = 5000;
16
- const AUTH_NEED = /authentication required/i;
17
- const PERMISSION_FAIL =
18
- /permissions?[ -]?(?:denied|denial)|denied permissions?/i;
19
- const GITDIR_PREFIX = 'gitdir:';
16
+ const CODEX_SANDBOX = 'sandbox_workspace_write';
17
+ const CODEX_ROOTS = 'writable_roots';
18
+ const AGY_SERVER = IS_WIN ? 'agy_acp_server.exe' : 'agy_acp_server.par';
19
+ const AGY_ARGS = process.platform === 'linux' ? ['--uid='] : [];
20
20
 
21
- const plainEffort = (level) => level;
22
- const codexEffort = (level) => `model_reasoning_effort=${level}`;
21
+ const realPath = (file) => {
22
+ try {
23
+ return fs.realpathSync.native(file);
24
+ } catch {
25
+ return path.resolve(file);
26
+ }
27
+ };
23
28
 
24
- const bashAllow = (entry) => {
25
- if (!entry.startsWith('Shell(') || !entry.endsWith(')')) return '';
26
- const bin = entry.slice('Shell('.length, -1).split(/\s+/)[0];
27
- if (!bin) return '';
28
- return `Bash(${bin} *)`;
29
+ // workspace-write remounts .git read-only unless that path is itself writable.
30
+ const gitWriteRoots = (cwd) => {
31
+ if (!cwd) return [];
32
+ const dirs = resolveGitDirs(cwd);
33
+ if (!dirs) return [];
34
+ const roots = [realPath(dirs.gitDir), realPath(dirs.commonDir)];
35
+ return [...new Set(roots)];
29
36
  };
30
37
 
31
- const BASH_ALLOWS = [...new Set(PLAN_ALLOWS.map(bashAllow).filter(Boolean))];
38
+ // The adapter merges this into the session config of codex
39
+ const codexEnv = (cwd, env = process.env) => {
40
+ const roots = gitWriteRoots(cwd);
41
+ if (!roots.length || env.CODEX_CONFIG) return {};
42
+ const config = { [CODEX_SANDBOX]: { [CODEX_ROOTS]: roots } };
43
+ return { CODEX_CONFIG: JSON.stringify(config) };
44
+ };
32
45
 
33
- // A piped session has no approval host. Preapproved shells can still run tests.
34
- const CLAUDE_PREFIX = [
35
- '--print',
36
- '--permission-mode',
37
- 'acceptEdits',
38
- '--permission-prompts',
39
- 'none',
40
- '--allowedTools',
41
- ...BASH_ALLOWS,
42
- ];
46
+ const antigravityEnv = (cwd, row) => {
47
+ if (!row || !row.bin) return {};
48
+ const name = IS_WIN ? 'localharness_external.exe' : 'localharness_external';
49
+ const helper = path.join(path.dirname(row.bin), name);
50
+ return fs.existsSync(helper) ? { ANTIGRAVITY_HARNESS_PATH: helper } : {};
51
+ };
43
52
 
53
+ // Each agent is reached through the command that serves ACP on its stdio:
54
+ // its own subcommand, or the adapter that wraps its SDK
44
55
  const defineAgent = (spec) => ({
45
56
  id: spec.id,
46
57
  name: spec.name ?? spec.id,
47
58
  title: spec.title,
48
- bins: spec.bins,
49
- models: spec.models,
50
- modelFlag: spec.modelFlag ?? '--model',
51
- effortFlag: spec.effortFlag ?? '',
52
- effortValue: spec.effortValue ?? plainEffort,
53
- efforts: spec.efforts ?? [],
54
- modelParams: spec.modelParams === true,
55
- prefix: spec.prefix ?? [],
56
- promptFlag: spec.promptFlag ?? '',
57
- login: spec.login ?? [],
58
- status: spec.status ?? [],
59
- contexts: spec.contexts ?? [],
60
- resumeWith: spec.resumeWith ?? null,
61
- listCmds: spec.listCmds,
62
- parseModels: spec.parseModels ?? parseNameList,
63
- wideModels: spec.wideModels ?? null,
64
- partialList: spec.partialList === true,
65
- modelEfforts: spec.modelEfforts ?? null,
66
- effortCmds: spec.effortCmds ?? [],
67
- allowlist: spec.allowlist === true,
68
- denied: spec.denied ?? null,
69
- savesExtra: spec.savesExtra === true,
59
+ commands: spec.commands,
60
+ package: spec.package ?? '',
61
+ registry: spec.registry ?? '',
62
+ installLabel: spec.installLabel ?? '',
63
+ hint: spec.hint ?? '',
64
+ mode: spec.mode ?? '',
65
+ env: spec.env ?? (() => ({})),
70
66
  });
71
67
 
72
68
  const AGENTS = [
73
69
  defineAgent({
74
70
  id: 'claude',
75
71
  title: 'Claude Code',
76
- bins: ['claude'],
77
- models: ['default', 'sonnet', 'opus', 'haiku', 'fable'],
78
- effortFlag: '--effort',
79
- efforts: ['default', 'low', 'medium', 'high', 'max'],
80
- prefix: CLAUDE_PREFIX,
81
- resumeWith: (id) => ['--resume', id],
82
- // No command lists models: the help names the aliases and the efforts
83
- listCmds: [['--help']],
84
- parseModels: parseHelpModels,
85
- partialList: true,
86
- effortCmds: [['--help']],
72
+ commands: [['claude-agent-acp'], ['claude-code-acp']],
73
+ package: '@agentclientprotocol/claude-agent-acp',
74
+ // Edits are accepted by the agent, the rest is asked of the run
75
+ mode: 'acceptEdits',
87
76
  }),
88
77
  defineAgent({
89
78
  id: 'opencode',
90
79
  title: 'OpenCode',
91
- bins: ['opencode'],
92
- models: ['default', 'sonnet', 'opus', 'gpt-5'],
93
- resumeWith: (id) => ['--session', id],
94
- listCmds: [['models']],
80
+ commands: [['opencode', 'acp']],
81
+ package: 'opencode-ai',
95
82
  }),
96
83
  defineAgent({
97
84
  id: 'cursor',
98
85
  title: 'Cursor',
99
- bins: ['cursor-agent', 'agent'],
100
- models: ['default', 'auto', 'grok-4.6', 'composer-2', 'gpt-5', 'sonnet'],
101
- modelParams: true,
102
- efforts: ['default', 'low', 'medium', 'high', 'xhigh'],
103
- prefix: ['--print', '--trust'],
104
- allowlist: true,
105
- login: ['login'],
106
- status: ['status', '--format', 'json'],
107
- contexts: ['256k', '1m'],
108
- resumeWith: (id) => [`--resume=${id}`],
109
- listCmds: [['--list-models'], ['models']],
110
- parseModels: parseCursorModels,
111
- wideModels: parseCursorWide,
86
+ commands: [
87
+ ['cursor-agent', 'acp'],
88
+ ['agent', 'acp'],
89
+ ],
90
+ hint: 'needs the Cursor CLI from cursor.com/cli',
112
91
  }),
113
92
  defineAgent({
114
93
  id: 'codex',
115
94
  title: 'Codex',
116
- bins: ['codex'],
117
- models: ['default', 'gpt-5.1-codex', 'gpt-5', 'o3'],
118
- effortFlag: '-c',
119
- effortValue: codexEffort,
120
- efforts: ['default', 'low', 'medium', 'high', 'xhigh'],
121
- // The TUI rejects a closed stdin; exec is the non-interactive entry
122
- prefix: ['exec'],
123
- resumeWith: (id) => ['resume', id],
124
- listCmds: [
125
- ['debug', 'models', '--bundled'],
126
- ['debug', 'models'],
127
- ['models'],
128
- ],
129
- parseModels: parseJsonModels,
130
- modelEfforts: parseJsonEfforts,
95
+ commands: [['codex-acp']],
96
+ package: '@agentclientprotocol/codex-acp',
97
+ mode: 'workspace-write',
98
+ env: codexEnv,
131
99
  }),
132
100
  defineAgent({
133
101
  id: 'agy',
134
102
  title: 'Antigravity',
135
- bins: ['agy'],
136
- models: [
137
- 'default',
138
- 'gemini-3.8-flash-high',
139
- 'gemini-3.8-flash-medium',
140
- 'gemini-3.8-flash-low',
141
- 'gemini-3.7-flash-high',
142
- 'gemini-3.7-flash-medium',
143
- 'gemini-3.7-flash-low',
144
- 'gemini-3.6-flash-high',
145
- 'gemini-3.6-flash-medium',
146
- 'gemini-3.6-flash-low',
147
- 'gemini-3.1-pro-high',
148
- 'gemini-3.1-pro-low',
149
- 'claude-sonnet-4-6',
150
- 'claude-opus-4-6-thinking',
151
- 'gpt-oss-120b-medium',
152
- ],
153
- effortFlag: '--effort',
154
- efforts: ['default', 'low', 'medium', 'high'],
155
- promptFlag: '-p',
156
- // Exits 0 after a refused tool call, so the output tells the outcome
157
- denied: PERMISSION_FAIL,
158
- // Extra flags in .reslop are the opt-in to run without confirmations
159
- savesExtra: true,
160
- resumeWith: (id) => ['--conversation', id],
161
- listCmds: [['models']],
162
- effortCmds: [['--help']],
103
+ commands: [[AGY_SERVER, ...AGY_ARGS], ['agy-acp']],
104
+ registry: 'antigravity-acp',
105
+ installLabel: 'Google Antigravity ACP',
106
+ hint: 'install Google Antigravity ACP',
107
+ env: antigravityEnv,
163
108
  }),
164
109
  ];
165
110
 
@@ -204,12 +149,25 @@ const findBin = (name, env = process.env) => {
204
149
  return '';
205
150
  };
206
151
 
207
- const findAgentBin = (spec, env) => {
208
- for (const name of spec.bins) {
209
- const found = findBin(name, env);
210
- if (found) return found;
152
+ const findCommand = (spec, env) => {
153
+ for (const [name, ...args] of spec.commands) {
154
+ const bin = findBin(name, env);
155
+ if (bin) return { bin, args };
211
156
  }
212
- return '';
157
+ if (spec.registry) {
158
+ const installed = installedRegistryAgent(spec.registry, { env });
159
+ if (installed) return installed;
160
+ }
161
+ return { bin: '', args: [] };
162
+ };
163
+
164
+ // What a missing agent takes: an npm package or an official registry binary
165
+ const installHint = (spec, env) => {
166
+ if (spec.package) return `npm i -g ${spec.package}`;
167
+ if (spec.registry && findBin('agy', env)) {
168
+ return 'agy installed; install Google Antigravity ACP';
169
+ }
170
+ return spec.hint;
213
171
  };
214
172
 
215
173
  const detectAgents = (env = process.env) =>
@@ -217,339 +175,115 @@ const detectAgents = (env = process.env) =>
217
175
  id: spec.id,
218
176
  name: spec.name,
219
177
  spec,
220
- models: spec.models.slice(),
221
- wide: null,
222
- listedEfforts: null,
223
- modelEfforts: null,
224
- bin: findAgentBin(spec, env),
178
+ package: spec.package,
179
+ installable: Boolean(spec.package || spec.registry),
180
+ installLabel: spec.installLabel,
181
+ hint: installHint(spec, env),
182
+ catalog: null,
183
+ ...findCommand(spec, env),
225
184
  }));
226
185
 
227
- const canLogin = (row) =>
228
- Boolean(row && row.bin && row.spec && row.spec.login.length);
229
-
230
- const tryList = async (run, bin, args, options) => {
231
- try {
232
- return await run(bin, args, options);
233
- } catch {
234
- return null;
235
- }
236
- };
237
-
238
- const listedOf = (listed) => {
239
- const bare = Array.isArray(listed);
240
- const given = bare ? { names: listed, wide: listed.wide } : listed;
241
- const from = given && typeof given === 'object' ? given : {};
242
- const efforts = Array.isArray(from.efforts) ? from.efforts : [];
243
- const perModel = from.modelEfforts instanceof Map ? from.modelEfforts : null;
244
- return {
245
- names: Array.isArray(from.names) ? from.names : [],
246
- wide: Array.isArray(from.wide) ? from.wide : null,
247
- efforts: efforts.length ? efforts : null,
248
- modelEfforts: perModel && perModel.size ? perModel : null,
249
- };
250
- };
251
-
252
- // One answer per command line, so a command that serves two lists runs once
253
- class CliProbe {
254
- constructor(bin, options) {
255
- this.bin = bin;
256
- this.run = options.run ?? runProc;
257
- this.env = { ...process.env, ...options.env };
258
- this.timeout = options.timeout ?? LIST_MS;
259
- this.texts = new Map();
260
- }
261
-
262
- async read(args) {
263
- const key = args.join(' ');
264
- if (this.texts.has(key)) return this.texts.get(key);
265
- const { env, timeout } = this;
266
- const result = await tryList(this.run, this.bin, args, { env, timeout });
267
- const { stdout = '', stderr = '' } = result ?? {};
268
- const text = result ? `${stdout}\n${stderr}` : '';
269
- this.texts.set(key, text);
270
- return text;
271
- }
272
-
273
- async first(cmds, parse) {
274
- for (const args of cmds) {
275
- const text = await this.read(args);
276
- const found = text ? parse(text) : [];
277
- if (found.length) return { text, found };
278
- }
279
- return null;
280
- }
281
- }
282
-
283
- const listModels = async (bin, spec, options = {}) => {
284
- const listed = { names: [], wide: null, efforts: null, modelEfforts: null };
285
- if (!bin || !spec) return listed;
286
- const probe = new CliProbe(bin, options);
287
- const models = await probe.first(spec.listCmds, spec.parseModels);
288
- if (models) {
289
- const { text } = models;
290
- listed.names = models.found;
291
- listed.wide = spec.wideModels ? spec.wideModels(text) : null;
292
- listed.modelEfforts = spec.modelEfforts ? spec.modelEfforts(text) : null;
293
- }
294
- const levels = (text) => optionValues(text, spec.effortFlag);
295
- const efforts = await probe.first(spec.effortCmds, levels);
296
- if (efforts) listed.efforts = efforts.found;
297
- return listed;
298
- };
299
-
300
- const splitArgs = (text) => trimText(text).split(/\s+/).filter(Boolean);
301
-
302
- const quoteArg = (text) => {
303
- const arg = `${text ?? ''}`;
304
- if (!arg) return '""';
305
- if (!/[\s"'\\]/.test(arg)) return arg;
306
- return `"${arg.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
307
- };
308
-
309
- const commandLine = (launch) => {
310
- if (!launch || !launch.ok || !launch.cmd) return '';
311
- const args = launch.args ?? [];
312
- const hasPrompt = launch.prompt && args.at(-1) === launch.prompt;
313
- const shown = hasPrompt ? args.slice(0, -1) : args;
314
- const plan = launch.plan ? quoteArg(launch.plan) : '';
315
- const bin = path.basename(launch.cmd);
316
- return [bin, ...shown.map(quoteArg), plan].filter(Boolean).join(' ');
186
+ const listModels = (row, options = {}) => {
187
+ if (!row || !row.bin) return Promise.resolve(null);
188
+ return probeSession(row, options);
317
189
  };
318
190
 
319
191
  const planRef = (reviewPath, cwd) => {
320
192
  const file = `${reviewPath ?? ''}`;
321
193
  if (!file || !cwd) return file;
322
- const rel = path.relative(cwd, file);
194
+ const rel = path.relative(cwd, path.resolve(cwd, file));
323
195
  if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return file;
324
196
  return rel.split(path.sep).join('/');
325
197
  };
326
198
 
327
- const planPrompt = (reviewPath) =>
328
- `Read and execute repair plan at ${reviewPath}`;
329
-
330
- const needsAuth = (text) => AUTH_NEED.test(`${text ?? ''}`);
331
-
332
- const parsedAuth = (raw) => {
333
- const start = raw.indexOf('{');
334
- const end = raw.lastIndexOf('}');
335
- if (start < 0 || end <= start) return null;
336
- const body = raw.slice(start, end + 1);
337
- const data = jsonParse(body);
338
- if (!data || typeof data !== 'object') return null;
339
- if (typeof data.isAuthenticated === 'boolean') return data.isAuthenticated;
340
- if (data.status === 'authenticated') return true;
341
- if (data.status === 'unauthenticated') return false;
342
- if (data.status === 'partially-authenticated') return false;
343
- return null;
344
- };
345
-
346
- const authState = (text) => {
347
- const raw = trimText(text);
348
- if (!raw) return null;
349
- const parsed = parsedAuth(raw);
350
- if (parsed !== null) return parsed;
351
- if (/not logged in/i.test(raw)) return false;
352
- if (/logged in/i.test(raw)) return true;
353
- return null;
354
- };
355
-
356
- const probeAuth = async (row) => {
357
- const args = row && row.spec ? row.spec.status : [];
358
- if (!row || !row.bin || !args.length) return null;
359
- const result = await runProc(row.bin, args, {
360
- env: commandEnv(),
361
- timeout: LIST_MS,
362
- });
363
- if (!result || result.error) return null;
364
- const text = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
365
- return authState(text);
366
- };
367
-
368
- const effortArgs = (spec, effort) => {
369
- const level = trimText(effort);
370
- if (!spec.effortFlag || !level || level === 'default') return [];
371
- return [spec.effortFlag, spec.effortValue(level)];
372
- };
373
-
374
- const statOf = (file) => {
375
- try {
376
- return fs.statSync(file);
377
- } catch {
378
- return null;
379
- }
380
- };
381
-
382
- const readFirstLine = (file) => {
383
- try {
384
- return fs.readFileSync(file, 'utf8').split('\n')[0] ?? '';
385
- } catch {
386
- return '';
387
- }
388
- };
389
-
390
- const realPath = (file) => {
391
- try {
392
- return fs.realpathSync.native(file);
393
- } catch {
394
- return path.resolve(file);
395
- }
396
- };
397
-
398
- const gitDirOf = (dot) => {
399
- const line = readFirstLine(dot).trim();
400
- if (!line.startsWith(GITDIR_PREFIX)) return '';
401
- const raw = line.slice(GITDIR_PREFIX.length).trim();
402
- if (!raw) return '';
403
- return path.resolve(path.dirname(dot), raw);
404
- };
405
-
406
- const commonGitDir = (gitDir) => {
407
- const raw = readFirstLine(path.join(gitDir, 'commondir')).trim();
408
- if (!raw) return gitDir;
409
- return path.resolve(gitDir, raw);
410
- };
411
-
412
- // workspace-write remounts .git read-only unless that path is itself writable.
413
- const gitWriteRoots = (cwd) => {
414
- if (!cwd) return [];
415
- const dot = path.join(path.resolve(cwd), '.git');
416
- const stat = statOf(dot);
417
- if (!stat) return [];
418
- if (stat.isDirectory()) return [realPath(dot)];
419
- if (!stat.isFile()) return [];
420
- const gitDir = gitDirOf(dot);
421
- if (!gitDir) return [];
422
- const roots = [realPath(gitDir), realPath(commonGitDir(gitDir))];
423
- return [...new Set(roots)];
424
- };
425
-
426
- const tomlQuote = (text) => {
427
- const escaped = text.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
428
- return `"${escaped}"`;
429
- };
430
-
431
- const codexGitArgs = (cwd) => {
432
- const roots = gitWriteRoots(cwd);
433
- if (!roots.length) return [];
434
- const listed = roots.map(tomlQuote).join(',');
435
- const value = `sandbox_workspace_write.writable_roots=[${listed}]`;
436
- return ['--sandbox', 'workspace-write', '-c', value];
437
- };
438
-
439
- const paramPart = (key, value) => {
440
- const text = trimText(value);
441
- if (!text) return '';
442
- return `${key}=${text}`;
443
- };
444
-
445
- const withParams = (model, parts) => {
446
- const body = parts.filter(Boolean).join(',');
447
- if (!body || !model || model === 'default') return model;
448
- return `${model}[${body}]`;
449
- };
199
+ // A refusal reads to an agent as a person saying no, and it stops to ask
200
+ const UNATTENDED = [
201
+ 'You are running unattended: nobody can answer questions or approve',
202
+ 'commands. Change files with your edit tools. The shell is limited to',
203
+ `${COMMANDS.join(', ')} and read-only filters such as cat, grep,`,
204
+ 'head and tail. When a command is refused, do the step another way',
205
+ 'and continue with the plan.',
206
+ ].join(' ');
450
207
 
451
- const effortParam = (model, effort) => {
452
- const level = trimText(effort);
453
- if (!level || level === 'default') return '';
454
- if (`${model ?? ''}`.endsWith(`-${level}`)) return '';
455
- return level;
456
- };
457
-
458
- const fastParam = (model, fast) => {
459
- if (fast !== true) return '';
460
- if (`${model ?? ''}`.endsWith('-fast')) return '';
461
- return 'true';
462
- };
208
+ const planPrompt = (reviewPath) =>
209
+ `Read and execute repair plan at ${reviewPath}\n\n${UNATTENDED}`;
463
210
 
464
- const modelArg = (spec, resolved, context, fast) => {
465
- const parts = [paramPart('context', context)];
466
- const brackets = spec.modelParams && !resolved.encoded;
467
- if (!brackets || resolved.params === false) {
468
- return withParams(resolved.model, parts);
469
- }
470
- const level = effortParam(resolved.model, resolved.effort);
471
- parts.push(paramPart('effort', level));
472
- parts.push(paramPart('fast', fastParam(resolved.model, fast)));
473
- return withParams(resolved.model, parts);
211
+ // What a run is shown as: the settings it was started with and its plan
212
+ const launchLabel = (launch) => {
213
+ const { choice } = launch;
214
+ const fast = choice.fast ? 'fast' : '';
215
+ const parts = [launch.name, choice.model, choice.effort, fast];
216
+ return [...parts, choice.context, launch.plan].filter(Boolean).join(' ');
474
217
  };
475
218
 
476
- const createLaunch = (row, kind, args, plan = '', prompt = '') => {
219
+ const buildLaunch = (row, picked, reviewPath, cwd, sessionId = '') => {
220
+ if (!row || !row.bin) return { ok: false, error: 'not installed' };
221
+ if (!reviewPath) return { ok: false, error: 'no plan' };
222
+ const plan = planRef(reviewPath, cwd) || reviewPath;
477
223
  const launch = {
478
224
  ok: true,
479
- cmd: row.bin,
480
- args,
481
- prompt,
482
- plan,
483
225
  id: row.id,
484
226
  name: row.name,
485
- kind,
227
+ kind: 'run',
228
+ cmd: row.bin,
229
+ args: (row.args ?? []).slice(),
230
+ mode: row.spec.mode,
231
+ env: row.spec.env(cwd, row),
232
+ choice: choiceOf(picked),
233
+ plan,
234
+ prompt: planPrompt(plan),
235
+ session: trimText(sessionId),
486
236
  command: '',
487
237
  };
488
- launch.command = commandLine(launch);
238
+ launch.command = launchLabel(launch);
489
239
  return launch;
490
240
  };
491
241
 
492
- const resumeArgs = (spec, sessionId) => {
493
- const id = `${sessionId ?? ''}`;
494
- if (!id || !spec.resumeWith) return [];
495
- return spec.resumeWith(id);
496
- };
497
-
498
- const buildLaunch = (row, choice, reviewPath, cwd, sessionId = '') => {
499
- if (!row || !row.bin) return { ok: false, error: 'not installed' };
500
- if (!reviewPath) return { ok: false, error: 'no plan' };
501
- const { spec } = row;
502
- const picked = choice ?? emptyChoice(row);
503
- const catalog = catalogOf(row.models);
504
- const resolved = catalog.withFast(catalog.resolve(picked), picked.fast);
505
- const asked = trimText(picked.context);
506
- const menu = contextMenu(resolved.model, row.wide, catalog, spec.contexts);
507
- const pass = menu.includes(asked) && asked !== menu[0] ? asked : '';
508
- const model = modelArg(spec, resolved, pass, picked.fast);
509
- const plan = planRef(reviewPath, cwd) || reviewPath;
510
- const prompt = planPrompt(plan);
511
- const continued = resumeArgs(spec, sessionId);
512
- const args = [...spec.prefix, ...continued];
513
- if (model && model !== 'default') args.push(spec.modelFlag, model);
514
- if (!spec.modelParams && !resolved.encoded) {
515
- args.push(...effortArgs(spec, resolved.effort));
242
+ // The package lands in the global prefix of npm, where PATH finds its command
243
+ const buildInstall = (row) => {
244
+ if (!row) return { ok: false, error: 'no agent' };
245
+ if (row.bin) return { ok: false, error: 'installed' };
246
+ const name = row.spec.package;
247
+ const registryId = row.spec.registry;
248
+ if (!name && !registryId) {
249
+ return { ok: false, error: row.hint || 'no installer' };
516
250
  }
517
- if (spec.id === 'codex') args.push(...codexGitArgs(cwd));
518
- args.push(...splitArgs(picked.extra));
519
- if (spec.promptFlag) args.push(spec.promptFlag);
520
- args.push(prompt);
521
- const launch = createLaunch(row, 'run', args, plan, prompt);
522
- const id = `${sessionId ?? ''}`;
523
- if (id) launch.session = id;
524
- return launch;
525
- };
526
-
527
- const buildLogin = (row) => {
528
- if (!row || !row.bin) return { ok: false, error: 'not installed' };
529
- if (!canLogin(row)) return { ok: false, error: 'no login' };
530
- return createLaunch(row, 'login', row.spec.login.slice());
531
- };
532
-
533
- // The review screen forces raw output. An agent needs the reduced test report.
534
- const agentEnv = () => {
535
- const env = commandEnv();
536
- delete env.RESLOP_OUTPUT;
537
- return env;
251
+ const npmArgs = name ? ['install', '--global', name] : [];
252
+ const args = registryId
253
+ ? [path.join(__dirname, 'registry.js'), registryId]
254
+ : npmArgs;
255
+ const cmd = registryId ? process.execPath : npmBin();
256
+ const command = registryId
257
+ ? `install ${row.spec.installLabel || row.name}`
258
+ : `npm ${npmArgs.join(' ')}`;
259
+ return {
260
+ ok: true,
261
+ id: row.id,
262
+ name: row.name,
263
+ kind: 'install',
264
+ installer: registryId ? 'registry' : 'npm',
265
+ cmd,
266
+ args,
267
+ plan: '',
268
+ session: '',
269
+ command,
270
+ };
538
271
  };
539
272
 
540
- const startPipe = (cwd, launch, onData, onClose) => {
541
- const options = spawnBase({ cwd, env: agentEnv(), detached: !IS_WIN });
273
+ const startInstall = (cwd, launch, onData, onClose) => {
274
+ const extra = { cwd, env: commandEnv(), detached: !IS_WIN };
275
+ const options =
276
+ launch.installer === 'npm' ? npmOpts(extra) : spawnBase(extra);
542
277
  const proc = new PipeProcess(onData, onClose);
543
278
  proc.start(launch.cmd, launch.args, options).closeInput();
544
279
  return proc;
545
280
  };
546
281
 
547
- // An agent that reads an allow list gets the plan entries before it runs
548
282
  const startAgent = (cwd, launch, onData, onClose) => {
549
- const spec = agentSpec(launch.id);
550
- const listed = launch.kind === 'run' && spec && spec.allowlist;
551
- if (listed) ensurePlanAllows();
552
- return startPipe(cwd, launch, onData, onClose);
283
+ if (launch.kind === 'install') {
284
+ return startInstall(cwd, launch, onData, onClose);
285
+ }
286
+ return startRun(cwd, launch, onData, onClose, launch.env);
553
287
  };
554
288
 
555
289
  module.exports = {
@@ -558,16 +292,9 @@ module.exports = {
558
292
  agentSpec,
559
293
  findBin,
560
294
  detectAgents,
561
- canLogin,
562
- listedOf,
563
295
  listModels,
564
- splitArgs,
565
296
  planPrompt,
566
- needsAuth,
567
- authState,
568
- probeAuth,
569
- commandLine,
570
297
  buildLaunch,
571
- buildLogin,
298
+ buildInstall,
572
299
  startAgent,
573
300
  };