reslop 0.2.7 → 0.3.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.
- package/CHANGELOG.md +17 -1
- package/README.md +16 -14
- package/bin/reslop.js +8 -5
- package/lib/agents/agents.js +504 -0
- package/lib/agents/allow.js +71 -0
- package/lib/agents/allow.json +16 -0
- package/lib/agents/models.js +460 -0
- package/lib/api.js +15 -12
- package/lib/cli.js +43 -68
- package/lib/{files.js → common/files.js} +48 -55
- package/lib/{find.js → common/find.js} +1 -1
- package/lib/common/format.js +101 -0
- package/lib/{utilities.js → common/process.js} +7 -95
- package/lib/common/tasks.json +27 -0
- package/lib/common/utilities.js +112 -0
- package/lib/dashboard/git.js +9 -16
- package/lib/dashboard/model.js +90 -49
- package/lib/dashboard/npm.js +17 -13
- package/lib/dashboard/tree.js +27 -30
- package/lib/{session → dashboard}/watch.js +36 -31
- package/lib/{workspace.js → dashboard/workspace.js} +23 -38
- package/lib/{deps.js → deps/deps.js} +31 -45
- package/lib/{deps-diff.js → deps/diff.js} +35 -25
- package/lib/{manifest.js → deps/manifest.js} +47 -30
- package/lib/diff/align.js +1 -2
- package/lib/diff/diff.js +2 -2
- package/lib/diff/display.js +29 -5
- package/lib/diff/inline.js +10 -14
- package/lib/diff/items.js +6 -4
- package/lib/diff/patch.js +39 -3
- package/lib/diff/unit.js +45 -16
- package/lib/{git-branches.js → git/branches.js} +53 -48
- package/lib/{git-deps.js → git/deps.js} +123 -96
- package/lib/git/git.js +71 -0
- package/lib/git/worktree.js +475 -0
- package/lib/highlight/bash.js +1 -2
- package/lib/highlight/csv.js +1 -2
- package/lib/highlight/dot.js +1 -2
- package/lib/{highlight.js → highlight/highlight.js} +42 -11
- package/lib/highlight/html.js +20 -18
- package/lib/highlight/json.js +1 -2
- package/lib/highlight/log.js +1 -2
- package/lib/highlight/txt.js +1 -2
- package/lib/{session → input}/actions.js +111 -48
- package/lib/{editor.js → input/editor.js} +7 -8
- package/lib/{keys.js → input/keys.js} +15 -43
- package/lib/input/line-edit.js +77 -0
- package/lib/input/menu-pick.js +70 -0
- package/lib/render/agents.js +640 -0
- package/lib/render/branches.js +4 -5
- package/lib/render/commits.js +54 -64
- package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
- package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
- package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
- package/lib/render/diff.js +166 -45
- package/lib/render/field.js +131 -0
- package/lib/render/files.js +9 -14
- package/lib/render/footer.js +383 -0
- package/lib/render/frame.js +123 -25
- package/lib/render/header.js +176 -0
- package/lib/render/log.js +319 -0
- package/lib/render/menu.js +60 -0
- package/lib/render/notes.js +16 -11
- package/lib/render/npm.js +322 -196
- package/lib/render/packages.js +9 -13
- package/lib/render/primitives.js +101 -15
- package/lib/render/render.js +7 -11
- package/lib/render/repos.js +13 -25
- package/lib/render/status.js +370 -0
- package/lib/render/status.json +85 -0
- package/lib/render/{dash-table.js → table.js} +13 -60
- package/lib/render/tiles.js +109 -48
- package/lib/render/unit.js +32 -13
- package/lib/{report-model.js → report/model.js} +4 -6
- package/lib/{report-parse.js → report/parse.js} +9 -14
- package/lib/{report-render.js → report/render.js} +46 -14
- package/lib/{report-run.js → report/run.js} +42 -30
- package/lib/{report-test.js → report/test.js} +56 -55
- package/lib/{report-tools.js → report/tools.js} +24 -33
- package/lib/review/agent-instructions.md +9 -0
- package/lib/review/format.js +426 -0
- package/lib/{review-merge.js → review/merge.js} +9 -8
- package/lib/review/review.js +190 -0
- package/lib/review/store.js +281 -0
- package/lib/review/templates.js +83 -0
- package/lib/runs/commands.js +14 -0
- package/lib/runs/logs.js +299 -0
- package/lib/runs/output.js +174 -0
- package/lib/runs/process.js +113 -0
- package/lib/{runs.js → runs/runs.js} +84 -36
- package/lib/runs/scripts.js +150 -0
- package/lib/session/agents/agents.js +958 -0
- package/lib/session/agents/job.js +320 -0
- package/lib/session/agents/jobs.js +355 -0
- package/lib/session/agents/options.js +153 -0
- package/lib/session/agents/plans.js +83 -0
- package/lib/session/branches.js +33 -39
- package/lib/session/change-actions.js +34 -100
- package/lib/session/commit.js +91 -52
- package/lib/session/compose.js +181 -145
- package/lib/session/dashboard.js +23 -22
- package/lib/session/dispatch.js +85 -21
- package/lib/session/files-pane.js +47 -26
- package/lib/session/find.js +16 -29
- package/lib/session/import.js +82 -0
- package/lib/session/items.js +8 -17
- package/lib/session/lifecycle.js +75 -72
- package/lib/session/load.js +2 -4
- package/lib/session/navigation.js +6 -3
- package/lib/session/npm/history.js +249 -0
- package/lib/session/npm/npm.js +815 -0
- package/lib/session/{packages.js → npm/packages.js} +21 -14
- package/lib/session/ops.js +125 -34
- package/lib/session/pointer.js +202 -10
- package/lib/session/prefs.js +112 -0
- package/lib/session/reads.js +109 -0
- package/lib/session/review.js +28 -10
- package/lib/session/run-panel.js +40 -0
- package/lib/{session.js → session/session.js} +156 -63
- package/lib/session/tasks.js +320 -65
- package/lib/session/templates.js +1 -1
- package/lib/session/terminal.js +94 -12
- package/lib/session/unit.js +35 -30
- package/lib/session/update.js +194 -6
- package/lib/session/view.js +65 -65
- package/lib/session/workspace.js +39 -78
- package/lib/{capabilities.js → source/capabilities.js} +3 -12
- package/lib/{github.js → source/github.js} +102 -32
- package/lib/{gitlab.js → source/gitlab.js} +87 -10
- package/lib/{remote.js → source/remote.js} +144 -40
- package/lib/source/review-threads.graphql +20 -0
- package/lib/source/source.js +74 -0
- package/lib/term/ansi.js +396 -0
- package/lib/{clipboard.js → term/clipboard.js} +3 -6
- package/lib/{select.js → term/select.js} +2 -2
- package/lib/term/themes/dark.json +122 -0
- package/lib/term/themes/light.json +122 -0
- package/lib/term/wide.txt +21 -0
- package/lib/{wrap.js → term/wrap.js} +4 -4
- package/package.json +2 -2
- package/lib/ansi.js +0 -545
- package/lib/detect.js +0 -34
- package/lib/diff/edit.js +0 -33
- package/lib/git-worktree.js +0 -455
- package/lib/git.js +0 -79
- package/lib/npm-commands.js +0 -522
- package/lib/render/chrome.js +0 -806
- package/lib/render/present.js +0 -19
- package/lib/render/report.js +0 -43
- package/lib/render/search.js +0 -25
- package/lib/review.js +0 -745
- package/lib/session/accessors.js +0 -26
- package/lib/session/edit-click.js +0 -57
- package/lib/session/npm.js +0 -452
- package/lib/session/progress.js +0 -41
- package/lib/source.js +0 -36
- package/lib/update.js +0 -192
- /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.0][] - 2026-10-12
|
|
6
|
+
|
|
7
|
+
- Execute review results and plan in cli agents
|
|
8
|
+
- Detect available agents and choose its model, effort, context
|
|
9
|
+
- Show agent execution and progress; sessions, history in TUI
|
|
10
|
+
- Import GitHub and GitLab issues into tasks
|
|
11
|
+
- Run tests in the background
|
|
12
|
+
- Codebase refactoring and optimization for simplicity and performance
|
|
13
|
+
- Async repository reads and writes
|
|
14
|
+
- Startup and diff rendering optimizations
|
|
15
|
+
- Rename `.review` to `.plan`
|
|
16
|
+
- Reorganize `.reslop` settings into sections
|
|
17
|
+
- Dashboard and run panel improvements
|
|
18
|
+
- Optional line numbers in diff and file views (ctrl+l)
|
|
19
|
+
|
|
5
20
|
## [0.2.7][] - 2026-10-03
|
|
6
21
|
|
|
7
22
|
- Add tiling view for multi-repository environment
|
|
@@ -172,7 +187,8 @@
|
|
|
172
187
|
- Feedback and todos that produce a repair plan for an agent
|
|
173
188
|
- Intra-line highlighting in unified, mixed, and side-by-side layouts
|
|
174
189
|
|
|
175
|
-
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.
|
|
190
|
+
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.3.0...HEAD
|
|
191
|
+
[0.3.0]: https://github.com/tshemsedinov/reslop/compare/v0.2.7...v0.3.0
|
|
176
192
|
[0.2.7]: https://github.com/tshemsedinov/reslop/compare/v0.2.6...v0.2.7
|
|
177
193
|
[0.2.6]: https://github.com/tshemsedinov/reslop/compare/v0.2.5...v0.2.6
|
|
178
194
|
[0.2.5]: https://github.com/tshemsedinov/reslop/compare/v0.2.4...v0.2.5
|
package/README.md
CHANGED
|
@@ -17,20 +17,23 @@ Open a change, leave findings and a backlog, and prepare a repair plan for the a
|
|
|
17
17
|
Review → Plan → Repair → Verify
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
- Live dashboard: files, diffs, tasks, branches, commits, run, and npm, updated in real time
|
|
20
|
+
- Live dashboard: files, diffs, tasks, branches, commits, run, agents, and npm, updated in real time
|
|
21
21
|
- Leave feedback, a project backlog, and in-place code proposals as a repair plan
|
|
22
22
|
- Review uncommitted git diffs, a given commit, a GitHub PR, or a GitLab MR
|
|
23
23
|
- File-scope review mode: not just diffs, with editor mode
|
|
24
24
|
- Stage, unstage, or revert each contiguous block of diff lines
|
|
25
25
|
- Auto-reload local diffs when files change, keeping the current screen
|
|
26
|
-
- Import GitHub PR and GitLab MR review comments into the local plan
|
|
27
|
-
- Commits list: brief and full, view a commit's diff, commit, amend, apply, edit, fixup, and delete
|
|
26
|
+
- Import GitHub PR and GitLab MR review comments, and GitHub/GitLab issues, into tasks and the local plan
|
|
27
|
+
- Commits list: brief and full, view a commit's diff, commit, update, amend, apply, edit, fixup, and delete
|
|
28
28
|
- Branches: checkout, create, rebase, delete, pull, and push
|
|
29
29
|
- npm scripts and bins: run, edit, and reorder; double-click to run; clean logs older than 5 days
|
|
30
30
|
- Reduced test and lint output for agents: failures and a short summary, not passing noise
|
|
31
31
|
- Review, renew, audit and update npm dependencies in `package.json` and the lockfile
|
|
32
32
|
- Auto-update reslop patch and minor releases; confirm a new major
|
|
33
|
-
- Work with git, npm and fetch in background
|
|
33
|
+
- Work with git, npm, and fetch in the background; run tests in the background while you review
|
|
34
|
+
- Execute the plan in local CLI agents: pick agent, model, and effort; watch progress; sessions, history, and continue or new runs
|
|
35
|
+
- Agent defaults and extras in `.reslop` (grouped settings sections)
|
|
36
|
+
- Optional line numbers in diff and file views (`Ctrl+L`)
|
|
34
37
|
|
|
35
38
|
## Install
|
|
36
39
|
|
|
@@ -47,9 +50,9 @@ Requires Node.js `>=18.15.0`.
|
|
|
47
50
|
- `reslop path/file` show only that path or file (starts on the file list)
|
|
48
51
|
- `reslop 7ac260c` show that commit (read-only)
|
|
49
52
|
- `reslop https://github.com/metarhia/metacom/pull/555` GitHub/GitLab PR/MR
|
|
50
|
-
- `reslop
|
|
53
|
+
- `reslop https://github.com/metarhia/metacom/issues/550` import GitHub/GitLab issue
|
|
54
|
+
- `reslop https://github.com/metarhia/metacom/issues` import all issues
|
|
51
55
|
- `reslop -r` read-only mode
|
|
52
|
-
- `reslop -light` light color theme (default dark)
|
|
53
56
|
- `reslop t -- <program> [args]` run one program and print a reduced report
|
|
54
57
|
|
|
55
58
|
Prefix each program in an npm script with `reslop t --`. Keep `&&` between those wraps, so each program is captured on its own and a failing step does not start the next one:
|
|
@@ -59,29 +62,28 @@ Prefix each program in an npm script with `reslop t --`. Keep `&&` between those
|
|
|
59
62
|
"test": "reslop t -- npm run -s lint && reslop t -- node --test"
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
The report is Markdown: failures, diagnostics, and a short summary. Passing results are omitted and similar problems are grouped. The reduced report and the raw output
|
|
65
|
+
The report is Markdown: failures, diagnostics, and a short summary. Passing results are omitted and similar problems are grouped. The reduced report is saved as `.log/<name>.log` and the raw output as `.log/<name>.raw`. The report names the raw file for an agent to read when a detail is missing. Set `RESLOP_OUTPUT=raw` to pass the command through unchanged. The review screen runs scripts in raw mode and applies its own filter once.
|
|
63
66
|
|
|
64
67
|
Run the full check with `npm t`. Run one or more test files with `reslop t -- node --test <files>`. New reviews include those two commands in the agent instructions.
|
|
65
68
|
|
|
66
|
-
`reslop t` records each run
|
|
69
|
+
`reslop t` records each run as `.log/<name>.json` beside those files so that the dashboard can show commands started by an agent in another terminal.
|
|
67
70
|
|
|
68
|
-
Reviews go in `.
|
|
71
|
+
Reviews go in `.plan/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
69
72
|
|
|
70
|
-
- `editing`:
|
|
71
|
-
- `ready`: ready for AI to work through the checkboxes
|
|
73
|
+
- `editing`: writing the review in reslop; AI works through the checkboxes
|
|
72
74
|
- `partial`: AI started; some items remain
|
|
73
|
-
- `done`: all items marked `[x]`
|
|
75
|
+
- `done`: all items marked `[x]`; adding an issue in reslop sets it back to `editing`
|
|
76
|
+
|
|
77
|
+
`ready` and `pending` in older reviews are read as `editing`.
|
|
74
78
|
|
|
75
79
|
## Future
|
|
76
80
|
|
|
77
|
-
- Import issues from GitHub and GitLab
|
|
78
81
|
- Search, blame, and file history in the review
|
|
79
82
|
- Security and code-quality audit; propose a repair plan
|
|
80
83
|
- Send anonymized code blocks for expert review
|
|
81
84
|
- Send questions and the repair plan to experts for approval
|
|
82
85
|
- Ask experts
|
|
83
86
|
- Apply refactoring skills
|
|
84
|
-
- Call agents, harnesses, and IDEs to execute prepared plans
|
|
85
87
|
- Squash, cherry-pick, and interactive rebase
|
|
86
88
|
- Merge, rename, and conflict resolution
|
|
87
89
|
|
package/bin/reslop.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
const { run, errorMessage } = cli;
|
|
6
|
-
const session = require('../lib/session.js');
|
|
7
|
-
const { LEAVE_TERM } = session;
|
|
4
|
+
const { errorMessage } = require('../lib/common/utilities.js');
|
|
8
5
|
|
|
9
6
|
const fail = (reason) => {
|
|
10
7
|
try {
|
|
8
|
+
const { LEAVE_TERM } = require('../lib/session/terminal.js');
|
|
11
9
|
process.stdout.write(LEAVE_TERM);
|
|
12
10
|
} catch {
|
|
13
11
|
// ignore
|
|
@@ -19,4 +17,9 @@ const fail = (reason) => {
|
|
|
19
17
|
process.on('uncaughtException', fail);
|
|
20
18
|
process.on('unhandledRejection', fail);
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
const start = () => {
|
|
21
|
+
if (process.argv[2] !== 't') return require('../lib/cli.js').run(process);
|
|
22
|
+
return require('../lib/report/run.js').runCapture(process, {});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
start().then((code) => process.exit(code), fail);
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
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');
|
|
8
|
+
const { commandEnv, PipeProcess } = require('../runs/process.js');
|
|
9
|
+
const { ensurePlanAllows, PLAN_ALLOWS } = require('./allow.js');
|
|
10
|
+
const models = require('./models.js');
|
|
11
|
+
const { parseNameList, parseCursorModels, parseCursorWide } = models;
|
|
12
|
+
const { parseJsonModels, catalogOf, emptyChoice, contextMenu } = models;
|
|
13
|
+
const { parseJsonEfforts, parseHelpModels, optionValues } = models;
|
|
14
|
+
|
|
15
|
+
const LIST_MS = 5000;
|
|
16
|
+
const AUTH_NEED = /authentication required/i;
|
|
17
|
+
const PERMISSION_FAIL =
|
|
18
|
+
/permissions?[ -]?(?:denied|denial)|denied permissions?/i;
|
|
19
|
+
|
|
20
|
+
const plainEffort = (level) => level;
|
|
21
|
+
const codexEffort = (level) => `model_reasoning_effort=${level}`;
|
|
22
|
+
|
|
23
|
+
const bashAllow = (entry) => {
|
|
24
|
+
if (!entry.startsWith('Shell(') || !entry.endsWith(')')) return '';
|
|
25
|
+
const bin = entry.slice('Shell('.length, -1).split(/\s+/)[0];
|
|
26
|
+
if (!bin) return '';
|
|
27
|
+
return `Bash(${bin} *)`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const BASH_ALLOWS = [...new Set(PLAN_ALLOWS.map(bashAllow).filter(Boolean))];
|
|
31
|
+
|
|
32
|
+
// A piped session has no approval host. Preapproved shells can still run tests.
|
|
33
|
+
const CLAUDE_PREFIX = [
|
|
34
|
+
'--print',
|
|
35
|
+
'--permission-mode',
|
|
36
|
+
'acceptEdits',
|
|
37
|
+
'--permission-prompts',
|
|
38
|
+
'none',
|
|
39
|
+
'--allowedTools',
|
|
40
|
+
...BASH_ALLOWS,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const defineAgent = (spec) => ({
|
|
44
|
+
id: spec.id,
|
|
45
|
+
name: spec.name ?? spec.id,
|
|
46
|
+
title: spec.title,
|
|
47
|
+
bins: spec.bins,
|
|
48
|
+
models: spec.models,
|
|
49
|
+
modelFlag: spec.modelFlag ?? '--model',
|
|
50
|
+
effortFlag: spec.effortFlag ?? '',
|
|
51
|
+
effortValue: spec.effortValue ?? plainEffort,
|
|
52
|
+
efforts: spec.efforts ?? [],
|
|
53
|
+
modelParams: spec.modelParams === true,
|
|
54
|
+
prefix: spec.prefix ?? [],
|
|
55
|
+
promptFlag: spec.promptFlag ?? '',
|
|
56
|
+
login: spec.login ?? [],
|
|
57
|
+
status: spec.status ?? [],
|
|
58
|
+
contexts: spec.contexts ?? [],
|
|
59
|
+
resumeWith: spec.resumeWith ?? null,
|
|
60
|
+
listCmds: spec.listCmds,
|
|
61
|
+
parseModels: spec.parseModels ?? parseNameList,
|
|
62
|
+
wideModels: spec.wideModels ?? null,
|
|
63
|
+
partialList: spec.partialList === true,
|
|
64
|
+
modelEfforts: spec.modelEfforts ?? null,
|
|
65
|
+
effortCmds: spec.effortCmds ?? [],
|
|
66
|
+
allowlist: spec.allowlist === true,
|
|
67
|
+
denied: spec.denied ?? null,
|
|
68
|
+
savesExtra: spec.savesExtra === true,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const AGENTS = [
|
|
72
|
+
defineAgent({
|
|
73
|
+
id: 'claude',
|
|
74
|
+
title: 'Claude Code',
|
|
75
|
+
bins: ['claude'],
|
|
76
|
+
models: ['default', 'sonnet', 'opus', 'haiku', 'fable'],
|
|
77
|
+
effortFlag: '--effort',
|
|
78
|
+
efforts: ['default', 'low', 'medium', 'high', 'max'],
|
|
79
|
+
prefix: CLAUDE_PREFIX,
|
|
80
|
+
resumeWith: (id) => ['--resume', id],
|
|
81
|
+
// No command lists models: the help names the aliases and the efforts
|
|
82
|
+
listCmds: [['--help']],
|
|
83
|
+
parseModels: parseHelpModels,
|
|
84
|
+
partialList: true,
|
|
85
|
+
effortCmds: [['--help']],
|
|
86
|
+
}),
|
|
87
|
+
defineAgent({
|
|
88
|
+
id: 'opencode',
|
|
89
|
+
title: 'OpenCode',
|
|
90
|
+
bins: ['opencode'],
|
|
91
|
+
models: ['default', 'sonnet', 'opus', 'gpt-5'],
|
|
92
|
+
resumeWith: (id) => ['--session', id],
|
|
93
|
+
listCmds: [['models']],
|
|
94
|
+
}),
|
|
95
|
+
defineAgent({
|
|
96
|
+
id: 'cursor',
|
|
97
|
+
title: 'Cursor',
|
|
98
|
+
bins: ['cursor-agent', 'agent'],
|
|
99
|
+
models: ['default', 'auto', 'grok-4.6', 'composer-2', 'gpt-5', 'sonnet'],
|
|
100
|
+
modelParams: true,
|
|
101
|
+
efforts: ['default', 'low', 'medium', 'high', 'xhigh'],
|
|
102
|
+
prefix: ['--print', '--trust'],
|
|
103
|
+
allowlist: true,
|
|
104
|
+
login: ['login'],
|
|
105
|
+
status: ['status', '--format', 'json'],
|
|
106
|
+
contexts: ['256k', '1m'],
|
|
107
|
+
resumeWith: (id) => [`--resume=${id}`],
|
|
108
|
+
listCmds: [['--list-models'], ['models']],
|
|
109
|
+
parseModels: parseCursorModels,
|
|
110
|
+
wideModels: parseCursorWide,
|
|
111
|
+
}),
|
|
112
|
+
defineAgent({
|
|
113
|
+
id: 'codex',
|
|
114
|
+
title: 'Codex',
|
|
115
|
+
bins: ['codex'],
|
|
116
|
+
models: ['default', 'gpt-5.1-codex', 'gpt-5', 'o3'],
|
|
117
|
+
effortFlag: '-c',
|
|
118
|
+
effortValue: codexEffort,
|
|
119
|
+
efforts: ['default', 'low', 'medium', 'high', 'xhigh'],
|
|
120
|
+
// The TUI rejects a closed stdin; exec is the non-interactive entry
|
|
121
|
+
prefix: ['exec'],
|
|
122
|
+
resumeWith: (id) => ['resume', id],
|
|
123
|
+
listCmds: [
|
|
124
|
+
['debug', 'models', '--bundled'],
|
|
125
|
+
['debug', 'models'],
|
|
126
|
+
['models'],
|
|
127
|
+
],
|
|
128
|
+
parseModels: parseJsonModels,
|
|
129
|
+
modelEfforts: parseJsonEfforts,
|
|
130
|
+
}),
|
|
131
|
+
defineAgent({
|
|
132
|
+
id: 'agy',
|
|
133
|
+
title: 'Antigravity',
|
|
134
|
+
bins: ['agy'],
|
|
135
|
+
models: [
|
|
136
|
+
'default',
|
|
137
|
+
'gemini-3.8-flash-high',
|
|
138
|
+
'gemini-3.8-flash-medium',
|
|
139
|
+
'gemini-3.8-flash-low',
|
|
140
|
+
'gemini-3.7-flash-high',
|
|
141
|
+
'gemini-3.7-flash-medium',
|
|
142
|
+
'gemini-3.7-flash-low',
|
|
143
|
+
'gemini-3.6-flash-high',
|
|
144
|
+
'gemini-3.6-flash-medium',
|
|
145
|
+
'gemini-3.6-flash-low',
|
|
146
|
+
'gemini-3.1-pro-high',
|
|
147
|
+
'gemini-3.1-pro-low',
|
|
148
|
+
'claude-sonnet-4-6',
|
|
149
|
+
'claude-opus-4-6-thinking',
|
|
150
|
+
'gpt-oss-120b-medium',
|
|
151
|
+
],
|
|
152
|
+
effortFlag: '--effort',
|
|
153
|
+
efforts: ['default', 'low', 'medium', 'high'],
|
|
154
|
+
promptFlag: '-p',
|
|
155
|
+
// Exits 0 after a refused tool call, so the output tells the outcome
|
|
156
|
+
denied: PERMISSION_FAIL,
|
|
157
|
+
// Extra flags in .reslop are the opt-in to run without confirmations
|
|
158
|
+
savesExtra: true,
|
|
159
|
+
resumeWith: (id) => ['--conversation', id],
|
|
160
|
+
listCmds: [['models']],
|
|
161
|
+
effortCmds: [['--help']],
|
|
162
|
+
}),
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const agentSpec = (id) => AGENTS.find((spec) => spec.id === id) ?? null;
|
|
166
|
+
|
|
167
|
+
const pathDirs = (env) => {
|
|
168
|
+
const raw = env.PATH ?? env.Path ?? '';
|
|
169
|
+
return raw.split(path.delimiter).filter(Boolean);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const winNames = (name, env) => {
|
|
173
|
+
const ext = env.PATHEXT || '.EXE;.CMD;.BAT;.COM';
|
|
174
|
+
const upper = name.toUpperCase();
|
|
175
|
+
const names = [name];
|
|
176
|
+
for (const item of ext.split(';').filter(Boolean)) {
|
|
177
|
+
const suffix = item.startsWith('.') ? item : `.${item}`;
|
|
178
|
+
if (upper.endsWith(suffix.toUpperCase())) return [name];
|
|
179
|
+
names.push(`${name}${suffix}`);
|
|
180
|
+
}
|
|
181
|
+
return names;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const binNames = (name, env) => (IS_WIN ? winNames(name, env) : [name]);
|
|
185
|
+
|
|
186
|
+
const isBin = (file) => {
|
|
187
|
+
try {
|
|
188
|
+
if (!fs.statSync(file).isFile()) return false;
|
|
189
|
+
if (!IS_WIN) fs.accessSync(file, fs.constants.X_OK);
|
|
190
|
+
return true;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const findBin = (name, env = process.env) => {
|
|
197
|
+
for (const dir of pathDirs(env)) {
|
|
198
|
+
for (const file of binNames(name, env)) {
|
|
199
|
+
const full = path.join(dir, file);
|
|
200
|
+
if (isBin(full)) return full;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return '';
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const findAgentBin = (spec, env) => {
|
|
207
|
+
for (const name of spec.bins) {
|
|
208
|
+
const found = findBin(name, env);
|
|
209
|
+
if (found) return found;
|
|
210
|
+
}
|
|
211
|
+
return '';
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const detectAgents = (env = process.env) =>
|
|
215
|
+
AGENTS.map((spec) => ({
|
|
216
|
+
id: spec.id,
|
|
217
|
+
name: spec.name,
|
|
218
|
+
spec,
|
|
219
|
+
models: spec.models.slice(),
|
|
220
|
+
wide: null,
|
|
221
|
+
listedEfforts: null,
|
|
222
|
+
modelEfforts: null,
|
|
223
|
+
bin: findAgentBin(spec, env),
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
const canLogin = (row) =>
|
|
227
|
+
Boolean(row && row.bin && row.spec && row.spec.login.length);
|
|
228
|
+
|
|
229
|
+
const tryList = async (run, bin, args, options) => {
|
|
230
|
+
try {
|
|
231
|
+
return await run(bin, args, options);
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const listedOf = (listed) => {
|
|
238
|
+
const bare = Array.isArray(listed);
|
|
239
|
+
const given = bare ? { names: listed, wide: listed.wide } : listed;
|
|
240
|
+
const from = given && typeof given === 'object' ? given : {};
|
|
241
|
+
const efforts = Array.isArray(from.efforts) ? from.efforts : [];
|
|
242
|
+
const perModel = from.modelEfforts instanceof Map ? from.modelEfforts : null;
|
|
243
|
+
return {
|
|
244
|
+
names: Array.isArray(from.names) ? from.names : [],
|
|
245
|
+
wide: Array.isArray(from.wide) ? from.wide : null,
|
|
246
|
+
efforts: efforts.length ? efforts : null,
|
|
247
|
+
modelEfforts: perModel && perModel.size ? perModel : null,
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// One answer per command line, so a command that serves two lists runs once
|
|
252
|
+
class CliProbe {
|
|
253
|
+
constructor(bin, options) {
|
|
254
|
+
this.bin = bin;
|
|
255
|
+
this.run = options.run ?? runProc;
|
|
256
|
+
this.env = { ...process.env, ...options.env };
|
|
257
|
+
this.timeout = options.timeout ?? LIST_MS;
|
|
258
|
+
this.texts = new Map();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async read(args) {
|
|
262
|
+
const key = args.join(' ');
|
|
263
|
+
if (this.texts.has(key)) return this.texts.get(key);
|
|
264
|
+
const { env, timeout } = this;
|
|
265
|
+
const result = await tryList(this.run, this.bin, args, { env, timeout });
|
|
266
|
+
const { stdout = '', stderr = '' } = result ?? {};
|
|
267
|
+
const text = result ? `${stdout}\n${stderr}` : '';
|
|
268
|
+
this.texts.set(key, text);
|
|
269
|
+
return text;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async first(cmds, parse) {
|
|
273
|
+
for (const args of cmds) {
|
|
274
|
+
const text = await this.read(args);
|
|
275
|
+
const found = text ? parse(text) : [];
|
|
276
|
+
if (found.length) return { text, found };
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const listModels = async (bin, spec, options = {}) => {
|
|
283
|
+
const listed = { names: [], wide: null, efforts: null, modelEfforts: null };
|
|
284
|
+
if (!bin || !spec) return listed;
|
|
285
|
+
const probe = new CliProbe(bin, options);
|
|
286
|
+
const models = await probe.first(spec.listCmds, spec.parseModels);
|
|
287
|
+
if (models) {
|
|
288
|
+
const { text } = models;
|
|
289
|
+
listed.names = models.found;
|
|
290
|
+
listed.wide = spec.wideModels ? spec.wideModels(text) : null;
|
|
291
|
+
listed.modelEfforts = spec.modelEfforts ? spec.modelEfforts(text) : null;
|
|
292
|
+
}
|
|
293
|
+
const levels = (text) => optionValues(text, spec.effortFlag);
|
|
294
|
+
const efforts = await probe.first(spec.effortCmds, levels);
|
|
295
|
+
if (efforts) listed.efforts = efforts.found;
|
|
296
|
+
return listed;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const splitArgs = (text) => trimText(text).split(/\s+/).filter(Boolean);
|
|
300
|
+
|
|
301
|
+
const quoteArg = (text) => {
|
|
302
|
+
const arg = `${text ?? ''}`;
|
|
303
|
+
if (!arg) return '""';
|
|
304
|
+
if (!/[\s"'\\]/.test(arg)) return arg;
|
|
305
|
+
return `"${arg.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const commandLine = (launch) => {
|
|
309
|
+
if (!launch || !launch.ok || !launch.cmd) return '';
|
|
310
|
+
const args = launch.args ?? [];
|
|
311
|
+
const hasPrompt = launch.prompt && args.at(-1) === launch.prompt;
|
|
312
|
+
const shown = hasPrompt ? args.slice(0, -1) : args;
|
|
313
|
+
const plan = launch.plan ? quoteArg(launch.plan) : '';
|
|
314
|
+
const bin = path.basename(launch.cmd);
|
|
315
|
+
return [bin, ...shown.map(quoteArg), plan].filter(Boolean).join(' ');
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const planRef = (reviewPath, cwd) => {
|
|
319
|
+
const file = `${reviewPath ?? ''}`;
|
|
320
|
+
if (!file || !cwd) return file;
|
|
321
|
+
const rel = path.relative(cwd, file);
|
|
322
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return file;
|
|
323
|
+
return rel.split(path.sep).join('/');
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const planPrompt = (reviewPath) =>
|
|
327
|
+
`Read and execute repair plan at ${reviewPath}`;
|
|
328
|
+
|
|
329
|
+
const needsAuth = (text) => AUTH_NEED.test(`${text ?? ''}`);
|
|
330
|
+
|
|
331
|
+
const parsedAuth = (raw) => {
|
|
332
|
+
const start = raw.indexOf('{');
|
|
333
|
+
const end = raw.lastIndexOf('}');
|
|
334
|
+
if (start < 0 || end <= start) return null;
|
|
335
|
+
const body = raw.slice(start, end + 1);
|
|
336
|
+
const data = jsonParse(body);
|
|
337
|
+
if (!data || typeof data !== 'object') return null;
|
|
338
|
+
if (typeof data.isAuthenticated === 'boolean') return data.isAuthenticated;
|
|
339
|
+
if (data.status === 'authenticated') return true;
|
|
340
|
+
if (data.status === 'unauthenticated') return false;
|
|
341
|
+
if (data.status === 'partially-authenticated') return false;
|
|
342
|
+
return null;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const authState = (text) => {
|
|
346
|
+
const raw = trimText(text);
|
|
347
|
+
if (!raw) return null;
|
|
348
|
+
const parsed = parsedAuth(raw);
|
|
349
|
+
if (parsed !== null) return parsed;
|
|
350
|
+
if (/not logged in/i.test(raw)) return false;
|
|
351
|
+
if (/logged in/i.test(raw)) return true;
|
|
352
|
+
return null;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const probeAuth = async (row) => {
|
|
356
|
+
const args = row && row.spec ? row.spec.status : [];
|
|
357
|
+
if (!row || !row.bin || !args.length) return null;
|
|
358
|
+
const result = await runProc(row.bin, args, {
|
|
359
|
+
env: commandEnv(),
|
|
360
|
+
timeout: LIST_MS,
|
|
361
|
+
});
|
|
362
|
+
if (!result || result.error) return null;
|
|
363
|
+
const text = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
364
|
+
return authState(text);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const effortArgs = (spec, effort) => {
|
|
368
|
+
const level = trimText(effort);
|
|
369
|
+
if (!spec.effortFlag || !level || level === 'default') return [];
|
|
370
|
+
return [spec.effortFlag, spec.effortValue(level)];
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const paramPart = (key, value) => {
|
|
374
|
+
const text = trimText(value);
|
|
375
|
+
if (!text) return '';
|
|
376
|
+
return `${key}=${text}`;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const withParams = (model, parts) => {
|
|
380
|
+
const body = parts.filter(Boolean).join(',');
|
|
381
|
+
if (!body || !model || model === 'default') return model;
|
|
382
|
+
return `${model}[${body}]`;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const effortParam = (model, effort) => {
|
|
386
|
+
const level = trimText(effort);
|
|
387
|
+
if (!level || level === 'default') return '';
|
|
388
|
+
if (`${model ?? ''}`.endsWith(`-${level}`)) return '';
|
|
389
|
+
return level;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const fastParam = (model, fast) => {
|
|
393
|
+
if (fast !== true) return '';
|
|
394
|
+
if (`${model ?? ''}`.endsWith('-fast')) return '';
|
|
395
|
+
return 'true';
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const modelArg = (spec, resolved, context, fast) => {
|
|
399
|
+
const parts = [paramPart('context', context)];
|
|
400
|
+
const plain = spec.modelParams && !resolved.encoded;
|
|
401
|
+
if (!plain) return withParams(resolved.model, parts);
|
|
402
|
+
const level = effortParam(resolved.model, resolved.effort);
|
|
403
|
+
parts.push(paramPart('effort', level));
|
|
404
|
+
parts.push(paramPart('fast', fastParam(resolved.model, fast)));
|
|
405
|
+
return withParams(resolved.model, parts);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const createLaunch = (row, kind, args, plan = '', prompt = '') => {
|
|
409
|
+
const launch = {
|
|
410
|
+
ok: true,
|
|
411
|
+
cmd: row.bin,
|
|
412
|
+
args,
|
|
413
|
+
prompt,
|
|
414
|
+
plan,
|
|
415
|
+
id: row.id,
|
|
416
|
+
name: row.name,
|
|
417
|
+
kind,
|
|
418
|
+
command: '',
|
|
419
|
+
};
|
|
420
|
+
launch.command = commandLine(launch);
|
|
421
|
+
return launch;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const resumeArgs = (spec, sessionId) => {
|
|
425
|
+
const id = `${sessionId ?? ''}`;
|
|
426
|
+
if (!id || !spec.resumeWith) return [];
|
|
427
|
+
return spec.resumeWith(id);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const buildLaunch = (row, choice, reviewPath, cwd, sessionId = '') => {
|
|
431
|
+
if (!row || !row.bin) return { ok: false, error: 'not installed' };
|
|
432
|
+
if (!reviewPath) return { ok: false, error: 'no plan' };
|
|
433
|
+
const { spec } = row;
|
|
434
|
+
const picked = choice ?? emptyChoice(row);
|
|
435
|
+
const catalog = catalogOf(row.models);
|
|
436
|
+
const resolved = catalog.withFast(catalog.resolve(picked), picked.fast);
|
|
437
|
+
const asked = trimText(picked.context);
|
|
438
|
+
const menu = contextMenu(resolved.model, row.wide, catalog, spec.contexts);
|
|
439
|
+
const pass = menu.includes(asked) && asked !== menu[0] ? asked : '';
|
|
440
|
+
const model = modelArg(spec, resolved, pass, picked.fast);
|
|
441
|
+
const plan = planRef(reviewPath, cwd) || reviewPath;
|
|
442
|
+
const prompt = planPrompt(plan);
|
|
443
|
+
const continued = resumeArgs(spec, sessionId);
|
|
444
|
+
const args = [...spec.prefix, ...continued];
|
|
445
|
+
if (model && model !== 'default') args.push(spec.modelFlag, model);
|
|
446
|
+
if (!spec.modelParams && !resolved.encoded) {
|
|
447
|
+
args.push(...effortArgs(spec, resolved.effort));
|
|
448
|
+
}
|
|
449
|
+
args.push(...splitArgs(picked.extra));
|
|
450
|
+
if (spec.promptFlag) args.push(spec.promptFlag);
|
|
451
|
+
args.push(prompt);
|
|
452
|
+
const launch = createLaunch(row, 'run', args, plan, prompt);
|
|
453
|
+
const id = `${sessionId ?? ''}`;
|
|
454
|
+
if (id) launch.session = id;
|
|
455
|
+
return launch;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const buildLogin = (row) => {
|
|
459
|
+
if (!row || !row.bin) return { ok: false, error: 'not installed' };
|
|
460
|
+
if (!canLogin(row)) return { ok: false, error: 'no login' };
|
|
461
|
+
return createLaunch(row, 'login', row.spec.login.slice());
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// The review screen forces raw output. An agent needs the reduced test report.
|
|
465
|
+
const agentEnv = () => {
|
|
466
|
+
const env = commandEnv();
|
|
467
|
+
delete env.RESLOP_OUTPUT;
|
|
468
|
+
return env;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const startPipe = (cwd, launch, onData, onClose) => {
|
|
472
|
+
const options = spawnBase({ cwd, env: agentEnv(), detached: !IS_WIN });
|
|
473
|
+
const proc = new PipeProcess(onData, onClose);
|
|
474
|
+
proc.start(launch.cmd, launch.args, options).closeInput();
|
|
475
|
+
return proc;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// An agent that reads an allow list gets the plan entries before it runs
|
|
479
|
+
const startAgent = (cwd, launch, onData, onClose) => {
|
|
480
|
+
const spec = agentSpec(launch.id);
|
|
481
|
+
const listed = launch.kind === 'run' && spec && spec.allowlist;
|
|
482
|
+
if (listed) ensurePlanAllows();
|
|
483
|
+
return startPipe(cwd, launch, onData, onClose);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
module.exports = {
|
|
487
|
+
...models,
|
|
488
|
+
AGENTS,
|
|
489
|
+
agentSpec,
|
|
490
|
+
findBin,
|
|
491
|
+
detectAgents,
|
|
492
|
+
canLogin,
|
|
493
|
+
listedOf,
|
|
494
|
+
listModels,
|
|
495
|
+
splitArgs,
|
|
496
|
+
planPrompt,
|
|
497
|
+
needsAuth,
|
|
498
|
+
authState,
|
|
499
|
+
probeAuth,
|
|
500
|
+
commandLine,
|
|
501
|
+
buildLaunch,
|
|
502
|
+
buildLogin,
|
|
503
|
+
startAgent,
|
|
504
|
+
};
|