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.
Files changed (158) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +123 -96
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +111 -48
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +15 -43
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +46 -14
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +9 -0
  81. package/lib/review/format.js +426 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +958 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -39
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +181 -145
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +85 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +75 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +156 -63
  120. package/lib/session/tasks.js +320 -65
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +2 -2
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /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.2.7...HEAD
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 for AI
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 -n` start a new review even if the latest is still editing
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 are both saved under `.log/`. 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.
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 in `.log/.runs/` so that the dashboard can show commands started by an agent in another terminal.
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 `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
71
+ Reviews go in `.plan/YYYY-MM-DD-NN.md` with frontmatter `status`:
69
72
 
70
- - `editing`: still writing the review in reslop
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 cli = require('../lib/cli.js');
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
- run(process).then((code) => process.exit(code), fail);
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
+ };