@syntax-syllogism/aloop 0.7.0 → 0.8.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.
- package/CHANGELOG.md +36 -26
- package/README.md +21 -6
- package/bin/eval.mjs +58 -0
- package/bin/loop.mjs +31 -2
- package/package.json +9 -3
- package/presets/work-item/README.md +9 -7
- package/prompts/fix-gate.md +17 -0
- package/src/adapters.mjs +17 -4
- package/src/backends/gitlab.mjs +5 -3
- package/src/command.mjs +5 -10
- package/src/config.mjs +7 -2
- package/src/eval.mjs +217 -0
- package/src/index.mjs +7 -0
- package/src/manifest.mjs +4 -0
- package/src/operations.mjs +120 -7
- package/src/pipeline.mjs +62 -19
- package/src/publish.mjs +4 -12
- package/src/reporter.mjs +37 -1
- package/src/runner.mjs +153 -21
- package/src/state.mjs +5 -1
- package/src/tui.mjs +171 -0
- package/src/types.d.ts +238 -0
- package/src/verdict.mjs +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,27 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.8.0] - 2026-09-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Eval harness for running code across multiple models
|
|
13
|
+
- JSDoc type checking for validation
|
|
14
|
+
- Live TUI renderer for interactive aloop runs
|
|
15
|
+
- aloop init command for project initialization
|
|
16
|
+
- Guided resume capability to repair failing gates
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- TUI renderer now runs even with --yes flag
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- JSDoc type contracts bound to production boundaries
|
|
25
|
+
- Fixed TUI rendering performance with diff-stat caching
|
|
26
|
+
- Resume repair and downstream attestation restored
|
|
27
|
+
|
|
28
|
+
## [0.7.0] - 2026-09-18
|
|
9
29
|
|
|
10
30
|
### Added
|
|
11
31
|
|
|
@@ -17,11 +37,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
37
|
Gemini has no reasoning-effort flag, so a configured `effort` is ignored for
|
|
18
38
|
it.
|
|
19
39
|
|
|
20
|
-
## [0.
|
|
40
|
+
## [0.6.2] - 2026-09-16
|
|
21
41
|
|
|
22
|
-
###
|
|
42
|
+
### Fixed
|
|
23
43
|
|
|
24
|
-
-
|
|
44
|
+
- Resolve Windows executables to correct on-disk casing
|
|
25
45
|
|
|
26
46
|
## [0.6.0] - 2026-09-14
|
|
27
47
|
|
|
@@ -66,6 +86,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
66
86
|
- Handle a failed Windows timeout teardown and isolate POSIX tests from
|
|
67
87
|
Windows CI.
|
|
68
88
|
|
|
89
|
+
## [0.5.2] - 2026-09-13
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
|
|
93
|
+
- Internal maintenance and tooling updates
|
|
94
|
+
|
|
95
|
+
## [0.5.1] - 2026-09-13
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
98
|
+
|
|
99
|
+
- Internal maintenance and tooling updates
|
|
100
|
+
|
|
69
101
|
## [0.5.0] - 2026-09-12
|
|
70
102
|
|
|
71
103
|
### Added
|
|
@@ -88,15 +120,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
88
120
|
- Wiki commit preservation with cron scheduling
|
|
89
121
|
- Output streaming for long-running phases to prevent false hang detection
|
|
90
122
|
|
|
91
|
-
## [Unreleased]
|
|
92
|
-
|
|
93
123
|
---
|
|
94
124
|
|
|
95
125
|
## Pre-extraction history
|
|
96
126
|
|
|
97
127
|
History from before the standalone extraction (changesets format), preserved for reference.
|
|
98
128
|
|
|
99
|
-
|
|
100
129
|
## 0.4.6
|
|
101
130
|
|
|
102
131
|
### Patch Changes
|
|
@@ -200,22 +229,3 @@ History from before the standalone extraction (changesets format), preserved for
|
|
|
200
229
|
rather than assuming `origin` in repos with several remotes.
|
|
201
230
|
- Prompts ship with the package and are overridable per phase from
|
|
202
231
|
`.loop/prompts/` in the consuming project.
|
|
203
|
-
|
|
204
|
-
## [0.6.2] - 2026-09-16
|
|
205
|
-
|
|
206
|
-
### Fixed
|
|
207
|
-
|
|
208
|
-
- Resolve Windows executables to correct on-disk casing
|
|
209
|
-
|
|
210
|
-
## [0.5.2] - 2026-09-13
|
|
211
|
-
|
|
212
|
-
### Changed
|
|
213
|
-
|
|
214
|
-
- Internal maintenance and tooling updates
|
|
215
|
-
|
|
216
|
-
## [0.5.1] - 2026-09-13
|
|
217
|
-
|
|
218
|
-
### Changed
|
|
219
|
-
|
|
220
|
-
- Internal maintenance and tooling updates
|
|
221
|
-
|
package/README.md
CHANGED
|
@@ -28,7 +28,13 @@ Interactive confirmations use the Git Bash/Windows console when available. Use
|
|
|
28
28
|
|
|
29
29
|
## Quick start
|
|
30
30
|
|
|
31
|
-
From a git repository
|
|
31
|
+
From a git repository, create the editable configuration and prompt overrides:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
aloop init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Review the generated `loop.config.mjs`, then start a task:
|
|
32
38
|
|
|
33
39
|
```sh
|
|
34
40
|
aloop --task "Add request tracing" --name request-tracing
|
|
@@ -67,7 +73,8 @@ complete guide, manifest schema, and verdict contract.
|
|
|
67
73
|
|
|
68
74
|
## Configuration
|
|
69
75
|
|
|
70
|
-
|
|
76
|
+
`aloop init` creates a commented `loop.config.mjs` in the repository being
|
|
77
|
+
operated on. Adjust it as needed; for example, a configuration can be:
|
|
71
78
|
|
|
72
79
|
```js
|
|
73
80
|
export default {
|
|
@@ -125,12 +132,13 @@ placeholder is unresolved.
|
|
|
125
132
|
For repositories using Markdown work items, install the bundled preset:
|
|
126
133
|
|
|
127
134
|
```sh
|
|
128
|
-
|
|
129
|
-
cp -r node_modules/@syntax-syllogism/aloop/presets/work-item/prompts/. .loop/prompts/
|
|
135
|
+
aloop init --preset work-item
|
|
130
136
|
```
|
|
131
137
|
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
This creates the preset's sample configuration and prompt overrides. Adapt its
|
|
139
|
+
branches and gates before use. `aloop init` also copies the default prompts, so
|
|
140
|
+
you can edit any `.loop/prompts/<phase>.md` immediately; see
|
|
141
|
+
[`docs/loop.md`](docs/loop.md#getting-started) for overwrite and preset details.
|
|
134
142
|
|
|
135
143
|
## Building up to unattended runs
|
|
136
144
|
|
|
@@ -141,6 +149,13 @@ Use this progression:
|
|
|
141
149
|
3. Exercise the review loop with `--max-rounds 2`.
|
|
142
150
|
4. Add `-y` only for task shapes whose behavior is already understood.
|
|
143
151
|
|
|
152
|
+
## Evaluation harness
|
|
153
|
+
|
|
154
|
+
`aloop-eval <spec.mjs>` runs a task corpus across a model/config matrix, each
|
|
155
|
+
cell in its own throwaway repository, and reports completion, escaped
|
|
156
|
+
defects, convergence, and cost per cell and per config. See
|
|
157
|
+
[`docs/eval.md`](docs/eval.md) for the spec file shape and reported fields.
|
|
158
|
+
|
|
144
159
|
## Development
|
|
145
160
|
|
|
146
161
|
```sh
|
package/bin/eval.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { isMainEntrypoint } from '../src/entrypoint.mjs';
|
|
7
|
+
import { formatEvalSummaryTable, formatEvalTable, runEval, summarizeEval } from '../src/eval.mjs';
|
|
8
|
+
|
|
9
|
+
export function usage() {
|
|
10
|
+
return [
|
|
11
|
+
'Usage: aloop-eval <spec.mjs> [options]',
|
|
12
|
+
'',
|
|
13
|
+
'Runs the task corpus and model/config matrix exported by <spec.mjs>',
|
|
14
|
+
'(a default export, or named `corpus` and `matrix` exports) through the',
|
|
15
|
+
'loop and reports completion, escaped defects, convergence, cost, and time.',
|
|
16
|
+
'',
|
|
17
|
+
'Options:',
|
|
18
|
+
' --json Emit machine-readable JSON instead of tables',
|
|
19
|
+
].join('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runEvalCli(argv, { cwd = process.cwd(), output = console } = {}) {
|
|
23
|
+
const { values, positionals } = parseArgs({
|
|
24
|
+
args: argv,
|
|
25
|
+
options: { json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' } },
|
|
26
|
+
allowPositionals: true,
|
|
27
|
+
strict: true,
|
|
28
|
+
});
|
|
29
|
+
if (values.help || !positionals.length) {
|
|
30
|
+
output.log(usage());
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const [specPath] = positionals;
|
|
34
|
+
const spec = await import(pathToFileURL(resolve(cwd, specPath)).href);
|
|
35
|
+
const { corpus, matrix } = spec.default ?? spec;
|
|
36
|
+
if (!Array.isArray(corpus) || !Array.isArray(matrix)) {
|
|
37
|
+
throw new Error(`${specPath} must export a task \`corpus\` array and a config \`matrix\` array.`);
|
|
38
|
+
}
|
|
39
|
+
const results = await runEval({ corpus, matrix });
|
|
40
|
+
const summaries = summarizeEval(results);
|
|
41
|
+
if (values.json) {
|
|
42
|
+
output.log(JSON.stringify({ results, summaries }, null, 2));
|
|
43
|
+
} else {
|
|
44
|
+
output.log(formatEvalTable(results));
|
|
45
|
+
output.log('');
|
|
46
|
+
output.log(formatEvalSummaryTable(summaries));
|
|
47
|
+
}
|
|
48
|
+
return { results, summaries };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isMainEntrypoint(import.meta.url)) {
|
|
52
|
+
try {
|
|
53
|
+
await runEvalCli(process.argv.slice(2));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(error.message);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
package/bin/loop.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
doctor,
|
|
11
11
|
getAggregateMetrics,
|
|
12
12
|
getRun,
|
|
13
|
+
init,
|
|
13
14
|
inspectRun,
|
|
14
15
|
listRuns,
|
|
15
16
|
resolveRunsDir,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
import { computeRunMetrics } from '../src/metrics.mjs';
|
|
18
19
|
import { runLoop } from '../src/pipeline.mjs';
|
|
19
20
|
|
|
20
|
-
const commands = new Set(['run', 'list', 'status', 'inspect', 'cancel', 'clean', 'doctor', 'metrics']);
|
|
21
|
+
const commands = new Set(['run', 'list', 'status', 'inspect', 'cancel', 'clean', 'doctor', 'metrics', 'init']);
|
|
21
22
|
|
|
22
23
|
export function parseLoopArgs(argv) {
|
|
23
24
|
const command = commands.has(argv[0]) ? argv[0] : 'run';
|
|
@@ -37,12 +38,17 @@ export function parseLoopArgs(argv) {
|
|
|
37
38
|
'max-rounds': { type: 'string' },
|
|
38
39
|
from: { type: 'string' },
|
|
39
40
|
resume: { type: 'boolean' },
|
|
41
|
+
note: { type: 'string' },
|
|
42
|
+
'note-file': { type: 'string' },
|
|
40
43
|
yes: { type: 'boolean', short: 'y' },
|
|
41
44
|
'no-worktree': { type: 'boolean' },
|
|
45
|
+
'no-tui': { type: 'boolean' },
|
|
42
46
|
'dry-run': { type: 'boolean' },
|
|
43
47
|
json: { type: 'boolean' },
|
|
44
48
|
metrics: { type: 'boolean' },
|
|
45
49
|
'older-than': { type: 'string' },
|
|
50
|
+
preset: { type: 'string' },
|
|
51
|
+
force: { type: 'boolean' },
|
|
46
52
|
help: { type: 'boolean', short: 'h' },
|
|
47
53
|
},
|
|
48
54
|
allowPositionals: true,
|
|
@@ -79,12 +85,17 @@ export function parseLoopArgs(argv) {
|
|
|
79
85
|
maxRounds,
|
|
80
86
|
from: values.from,
|
|
81
87
|
resume: values.resume,
|
|
88
|
+
note: values.note,
|
|
89
|
+
noteFile: values['note-file'],
|
|
82
90
|
yes: values.yes,
|
|
83
91
|
noWorktree: values['no-worktree'],
|
|
92
|
+
noTui: values['no-tui'],
|
|
84
93
|
dryRun: values['dry-run'],
|
|
85
94
|
json: values.json,
|
|
86
95
|
metrics: values.metrics,
|
|
87
96
|
olderThanDays,
|
|
97
|
+
preset: values.preset,
|
|
98
|
+
force: values.force,
|
|
88
99
|
};
|
|
89
100
|
}
|
|
90
101
|
|
|
@@ -100,6 +111,7 @@ export function usage() {
|
|
|
100
111
|
' cancel <name> Stop a run and release its lock',
|
|
101
112
|
' clean Preview/remove completed runs older than 30 days',
|
|
102
113
|
' doctor Check configuration and local tooling',
|
|
114
|
+
' init Scaffold loop.config.mjs and .loop/prompts',
|
|
103
115
|
' run Start or resume a run (the default)',
|
|
104
116
|
'',
|
|
105
117
|
'Operational options:',
|
|
@@ -109,6 +121,10 @@ export function usage() {
|
|
|
109
121
|
' -y, --yes Confirm destructive clean operations',
|
|
110
122
|
' --dry-run Preview clean operations without changing files',
|
|
111
123
|
'',
|
|
124
|
+
'Init options:',
|
|
125
|
+
' --preset <name> Seed init from a bundled preset (e.g. work-item)',
|
|
126
|
+
' --force Overwrite existing scaffold files',
|
|
127
|
+
'',
|
|
112
128
|
'Run options:',
|
|
113
129
|
' -t, --task <text> Inline task description',
|
|
114
130
|
' -f, --task-file <path> Path to a task/plan/spec file',
|
|
@@ -122,7 +138,10 @@ export function usage() {
|
|
|
122
138
|
' --max-rounds <n> Cap on review/repair rounds',
|
|
123
139
|
' --from <phase> Start at this phase',
|
|
124
140
|
' --resume Skip phases already recorded complete',
|
|
141
|
+
' --note <text> Give the resumed phase an authoritative instruction',
|
|
142
|
+
' --note-file <path> Read the instruction from a file (--note wins)',
|
|
125
143
|
' --no-worktree Work in the current checkout instead of a worktree',
|
|
144
|
+
' --no-tui Force plain streaming output (TUI is on by default on a TTY)',
|
|
126
145
|
' -y, --yes Run unattended (no per-phase confirmation; required without a terminal)',
|
|
127
146
|
' --dry-run Print the plan and rendered prompts, run nothing',
|
|
128
147
|
].join('\n');
|
|
@@ -258,6 +277,16 @@ export async function runOperationalCommand(args, { cwd = process.cwd(), output
|
|
|
258
277
|
}
|
|
259
278
|
return result;
|
|
260
279
|
}
|
|
280
|
+
if (args.command === 'init') {
|
|
281
|
+
const result = await init({ cwd, preset: args.preset, force: args.force });
|
|
282
|
+
if (args.json) output.log(JSON.stringify(result, null, 2));
|
|
283
|
+
else {
|
|
284
|
+
for (const file of result.created) output.log(` create ${file}`);
|
|
285
|
+
for (const file of result.skipped) output.log(` skip ${file} (exists)`);
|
|
286
|
+
output.log(result.preset ? `Initialized aloop with preset "${result.preset}".` : 'Initialized aloop.');
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
261
290
|
throw new Error(`Unknown command: ${args.command}`);
|
|
262
291
|
}
|
|
263
292
|
|
|
@@ -270,7 +299,7 @@ if (isMainEntrypoint(import.meta.url)) {
|
|
|
270
299
|
await runLoop({ args });
|
|
271
300
|
} else {
|
|
272
301
|
const result = await runOperationalCommand(args);
|
|
273
|
-
if (args.command === 'doctor' && !result.ok) process.exitCode = 1;
|
|
302
|
+
if (args.command === 'doctor' && !(/** @type {any} */ (result)).ok) process.exitCode = 1;
|
|
274
303
|
}
|
|
275
304
|
} catch (error) {
|
|
276
305
|
console.error(error.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syntax-syllogism/aloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Syntax & Syllogism agentic loop runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"node": ">=22"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"aloop": "bin/loop.mjs"
|
|
16
|
+
"aloop": "bin/loop.mjs",
|
|
17
|
+
"aloop-eval": "bin/eval.mjs"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
"bin",
|
|
@@ -48,6 +49,11 @@
|
|
|
48
49
|
"access": "public"
|
|
49
50
|
},
|
|
50
51
|
"scripts": {
|
|
51
|
-
"test": "node --test"
|
|
52
|
+
"test": "node --test",
|
|
53
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^22.20.4",
|
|
57
|
+
"typescript": "^5.9.3"
|
|
52
58
|
}
|
|
53
59
|
}
|
|
@@ -18,14 +18,16 @@ branches, engines, phases, and `npm test` gate. Adjust it for your repository.
|
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
20
20
|
|
|
21
|
-
Install the package,
|
|
22
|
-
the project override directory:
|
|
21
|
+
Install the package, then initialize the project with the preset:
|
|
23
22
|
|
|
24
23
|
```sh
|
|
25
|
-
|
|
26
|
-
cp -r node_modules/@syntax-syllogism/aloop/presets/work-item/prompts/. .loop/prompts/
|
|
24
|
+
aloop init --preset work-item
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
This writes the sample config and preset prompts into your project, with the
|
|
28
|
+
packaged defaults filling any prompts the preset does not provide. The runner
|
|
29
|
+
loads project prompt overrides before its packaged defaults.
|
|
30
|
+
|
|
31
|
+
If you prefer to wire it up by hand, create `.loop/prompts/` and copy the
|
|
32
|
+
contents of `node_modules/@syntax-syllogism/aloop/presets/work-item/prompts/`
|
|
33
|
+
there; copy `loop.config.mjs` separately if its settings match your repository.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{{TASK_CONTEXT}}
|
|
2
|
+
|
|
3
|
+
The deterministic gate is failing. Work in `{{REPO}}` on branch `{{BRANCH}}`
|
|
4
|
+
and make the gate pass by fixing the underlying implementation.
|
|
5
|
+
|
|
6
|
+
## Gate status
|
|
7
|
+
|
|
8
|
+
{{GATE_STATUS}}
|
|
9
|
+
|
|
10
|
+
## Gate commands
|
|
11
|
+
|
|
12
|
+
{{GATE_COMMANDS}}
|
|
13
|
+
|
|
14
|
+
Do not delete, skip, or weaken tests or gate commands to make the gate pass.
|
|
15
|
+
Make only the changes needed to correct the failure, run the relevant tests and
|
|
16
|
+
the gate commands, then commit the repair with a conventional commit message.
|
|
17
|
+
Keep the worktree clean for the re-check.
|
package/src/adapters.mjs
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** @typedef {import('./types.js').Adapter} Adapter */
|
|
2
|
+
/** @typedef {import('./types.js').Config} Config */
|
|
3
|
+
/** @typedef {import('./types.js').Engine} Engine */
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Engine adapters.
|
|
3
7
|
*
|
|
@@ -305,15 +309,17 @@ export function createJsonlRenderer(renderEvent) {
|
|
|
305
309
|
}
|
|
306
310
|
|
|
307
311
|
/** A renderer for engines whose stdout is already meant to be read. */
|
|
312
|
+
/** @returns {import('./types.js').Renderer} */
|
|
308
313
|
export function passthroughRenderer() {
|
|
309
314
|
return { write: (text) => text, end: () => '' };
|
|
310
315
|
}
|
|
311
316
|
|
|
317
|
+
/** @type {Adapter} */
|
|
312
318
|
const claudeAdapter = {
|
|
313
319
|
name: 'claude',
|
|
314
320
|
version: cliVersion('claude'),
|
|
315
321
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
316
|
-
command({ prompt, addDirs, permissions, artifactOnly = false, agent = {} }) {
|
|
322
|
+
command({ prompt, addDirs, permissions, artifactOnly = false, agent = /** @type {import('./types.js').Agent} */ ({}) }) {
|
|
317
323
|
// `auto`, not `acceptEdits`, is used for worktree-writing and
|
|
318
324
|
// artifact-only phases: under acceptEdits a non-interactive `-p` run
|
|
319
325
|
// auto-denies every Bash call, because there is no one to answer the
|
|
@@ -334,11 +340,12 @@ const claudeAdapter = {
|
|
|
334
340
|
createRenderer: () => createJsonlRenderer(renderClaudeEvent),
|
|
335
341
|
};
|
|
336
342
|
|
|
343
|
+
/** @type {Adapter} */
|
|
337
344
|
const codexAdapter = {
|
|
338
345
|
name: 'codex',
|
|
339
346
|
version: cliVersion('codex'),
|
|
340
347
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
341
|
-
command({ prompt, cwd, addDirs, permissions, artifactOnly = false, agent = {} }) {
|
|
348
|
+
command({ prompt, cwd, addDirs, permissions, artifactOnly = false, agent = /** @type {import('./types.js').Agent} */ ({}) }) {
|
|
342
349
|
const canWrite = permissionLevel(permissions) === PERMISSIONS.WRITE_WORKTREE || artifactOnly;
|
|
343
350
|
const sandbox = canWrite ? 'workspace-write' : 'read-only';
|
|
344
351
|
const args = ['exec', prompt, '--sandbox', sandbox, '--cd', cwd];
|
|
@@ -349,11 +356,12 @@ const codexAdapter = {
|
|
|
349
356
|
},
|
|
350
357
|
};
|
|
351
358
|
|
|
359
|
+
/** @type {Adapter} */
|
|
352
360
|
const agyAdapter = {
|
|
353
361
|
name: 'agy',
|
|
354
362
|
version: cliVersion('agy'),
|
|
355
363
|
efforts: ['low', 'medium', 'high'],
|
|
356
|
-
command({ prompt, addDirs, timeoutMs, permissions, artifactOnly = false, agent = {} }) {
|
|
364
|
+
command({ prompt, addDirs, timeoutMs, permissions, artifactOnly = false, agent = /** @type {import('./types.js').Agent} */ ({}) }) {
|
|
357
365
|
// `--dangerously-skip-permissions`, not bare `accept-edits`: in headless
|
|
358
366
|
// `--print` mode agy cannot prompt for the `command` permission its Bash-
|
|
359
367
|
// style tools need, so it auto-denies the first one and exits 0 having done
|
|
@@ -382,12 +390,13 @@ const agyAdapter = {
|
|
|
382
390
|
createRenderer: () => createJsonlRenderer(renderAgyEvent),
|
|
383
391
|
};
|
|
384
392
|
|
|
393
|
+
/** @type {Adapter} */
|
|
385
394
|
const geminiAdapter = {
|
|
386
395
|
name: 'gemini',
|
|
387
396
|
version: cliVersion('gemini'),
|
|
388
397
|
// Gemini CLI exposes no reasoning-effort flag, so this adapter declares no
|
|
389
398
|
// `efforts`: a configured effort is simply not passed through (see docs).
|
|
390
|
-
command({ prompt, addDirs, permissions, artifactOnly = false, agent = {} }) {
|
|
399
|
+
command({ prompt, addDirs, permissions, artifactOnly = false, agent = /** @type {import('./types.js').Agent} */ ({}) }) {
|
|
391
400
|
// `yolo`, not `plan`/`auto_edit`: in headless `--prompt` mode Gemini cannot
|
|
392
401
|
// interactively approve the shell tools a worktree-writing phase needs to
|
|
393
402
|
// build, test, and commit. `plan` is read-only and `auto_edit` auto-approves
|
|
@@ -436,6 +445,7 @@ function validateConfiguredAdapter(name, adapter) {
|
|
|
436
445
|
return adapter;
|
|
437
446
|
}
|
|
438
447
|
|
|
448
|
+
/** @returns {Adapter} */
|
|
439
449
|
export function adapterFor(engine, customAdapters = {}) {
|
|
440
450
|
const configuredNames = customAdapters && typeof customAdapters === 'object' ? Object.keys(customAdapters) : [];
|
|
441
451
|
const hasConfiguredAdapter = customAdapters && typeof customAdapters === 'object'
|
|
@@ -448,6 +458,7 @@ export function adapterFor(engine, customAdapters = {}) {
|
|
|
448
458
|
return adapter;
|
|
449
459
|
}
|
|
450
460
|
|
|
461
|
+
/** @param {Engine} agent @returns {Engine} */
|
|
451
462
|
export function validateAgent(agent, customAdapters = {}) {
|
|
452
463
|
const adapter = adapterFor(agent.name, customAdapters);
|
|
453
464
|
if (agent.effort && adapter.efforts && !adapter.efforts.includes(agent.effort)) {
|
|
@@ -457,11 +468,13 @@ export function validateAgent(agent, customAdapters = {}) {
|
|
|
457
468
|
}
|
|
458
469
|
|
|
459
470
|
/** Resolve an agent descriptor: per-phase override, else the default. */
|
|
471
|
+
/** @param {Config} config @returns {Engine} */
|
|
460
472
|
export function agentForPhase(config, phaseName) {
|
|
461
473
|
return config.engines[phaseName] ?? config.engines.default;
|
|
462
474
|
}
|
|
463
475
|
|
|
464
476
|
/** Resolve which executable runs a phase. Retained for string-based callers. */
|
|
477
|
+
/** @param {Config} config */
|
|
465
478
|
export function engineForPhase(config, phaseName) {
|
|
466
479
|
return agentForPhase(config, phaseName).name;
|
|
467
480
|
}
|
package/src/backends/gitlab.mjs
CHANGED
|
@@ -83,7 +83,8 @@ function errorText(error) {
|
|
|
83
83
|
* The default GitLab transport. `glab` is intentionally an external runtime
|
|
84
84
|
* requirement, so consumers can replace this transport with REST or MCP code.
|
|
85
85
|
*/
|
|
86
|
-
export function glabTransport(
|
|
86
|
+
export function glabTransport(options = {}) {
|
|
87
|
+
const { cwd, runner = runCommand, env } = /** @type {any} */ (options);
|
|
87
88
|
async function run(args) {
|
|
88
89
|
try {
|
|
89
90
|
return await runner('glab', args, { cwd, ...(env ? { env } : {}) });
|
|
@@ -153,9 +154,10 @@ export function glabTransport({ cwd, runner = runCommand, env } = {}) {
|
|
|
153
154
|
/**
|
|
154
155
|
* Adapt GitLab-native merge request data to aloop's verified pull-request port.
|
|
155
156
|
*
|
|
156
|
-
* @param {
|
|
157
|
+
* @param {any} options
|
|
157
158
|
*/
|
|
158
|
-
export function gitlabBackend(
|
|
159
|
+
export function gitlabBackend(options = {}) {
|
|
160
|
+
let { cwd, git = new GitFacade(cwd), transport, env } = options;
|
|
159
161
|
transport ??= glabTransport({ cwd, env });
|
|
160
162
|
const repositories = new Map();
|
|
161
163
|
|
package/src/command.mjs
CHANGED
|
@@ -2,11 +2,8 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { existsSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { win32 as windowsPath } from 'node:path';
|
|
4
4
|
|
|
5
|
-
export function signalProcessGroup(pid, signal, {
|
|
6
|
-
platform = process.platform,
|
|
7
|
-
spawnImpl = spawn,
|
|
8
|
-
onFailure,
|
|
9
|
-
} = {}) {
|
|
5
|
+
export function signalProcessGroup(pid, signal, options = {}) {
|
|
6
|
+
const { platform = process.platform, spawnImpl = spawn, onFailure } = /** @type {any} */ (options);
|
|
10
7
|
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) return false;
|
|
11
8
|
if (platform === 'win32') {
|
|
12
9
|
const handleFailure = onFailure ?? (() => {
|
|
@@ -63,10 +60,8 @@ function environmentValue(env, name) {
|
|
|
63
60
|
return key ? env[key] : undefined;
|
|
64
61
|
}
|
|
65
62
|
|
|
66
|
-
export function resolveWindowsExecutable(command, env = process.env, {
|
|
67
|
-
fileExists = existsSync,
|
|
68
|
-
realPath = realpathSync.native,
|
|
69
|
-
} = {}) {
|
|
63
|
+
export function resolveWindowsExecutable(command, env = process.env, options = {}) {
|
|
64
|
+
const { fileExists = existsSync, realPath = realpathSync.native } = /** @type {any} */ (options);
|
|
70
65
|
const pathEntries = (environmentValue(env, 'PATH') ?? '').split(';').filter(Boolean);
|
|
71
66
|
const pathExtensions = (environmentValue(env, 'PATHEXT') ?? WINDOWS_EXECUTABLE_EXTENSIONS.join(';'))
|
|
72
67
|
.split(';')
|
|
@@ -144,7 +139,7 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
144
139
|
platform = process.platform,
|
|
145
140
|
spawnImpl = spawn,
|
|
146
141
|
resolveExecutable = resolveWindowsExecutable,
|
|
147
|
-
} = options;
|
|
142
|
+
} = /** @type {any} */ (options);
|
|
148
143
|
return new Promise((resolve, reject) => {
|
|
149
144
|
const spawnSpec = platform === 'win32' ? windowsSpawnSpec(command, args, env, resolveExecutable) : { command, args };
|
|
150
145
|
const child = spawnImpl(spawnSpec.command, spawnSpec.args, {
|
package/src/config.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** @typedef {import('./types.js').Config} Config */
|
|
2
|
+
/** @typedef {import('./types.js').PhaseDescriptor} PhaseDescriptor */
|
|
3
|
+
|
|
1
4
|
import { access } from 'node:fs/promises';
|
|
2
5
|
import { constants } from 'node:fs';
|
|
3
6
|
import { join, resolve } from 'node:path';
|
|
@@ -184,6 +187,7 @@ function toPhase(entry, index) {
|
|
|
184
187
|
* A custom verdict phase must declare its own `repair` list; merely placing a
|
|
185
188
|
* repair phase after it no longer changes control flow.
|
|
186
189
|
*/
|
|
190
|
+
/** @returns {PhaseDescriptor[]} */
|
|
187
191
|
export function normalizePhases(entries, { maxRounds }) {
|
|
188
192
|
const list = entries.map(toPhase);
|
|
189
193
|
const phaseIndexes = new Map();
|
|
@@ -215,7 +219,7 @@ export function normalizePhases(entries, { maxRounds }) {
|
|
|
215
219
|
|
|
216
220
|
for (let index = 0; index < list.length; index += 1) {
|
|
217
221
|
const phase = list[index];
|
|
218
|
-
if (!phase.verdict) continue;
|
|
222
|
+
if (!phase.verdict && !(phase.kind === 'gate' && phase.repair)) continue;
|
|
219
223
|
const repair = phase.repair ?? [];
|
|
220
224
|
if (!Array.isArray(repair)) {
|
|
221
225
|
throw new Error(`Phase "${phase.name}" has an invalid repair transition; expected an array.`);
|
|
@@ -233,7 +237,7 @@ export function normalizePhases(entries, { maxRounds }) {
|
|
|
233
237
|
for (let index = 0; index < list.length; index += 1) {
|
|
234
238
|
const phase = list[index];
|
|
235
239
|
if (phase.role === 'repair' && !consumedRepairs.has(index)) {
|
|
236
|
-
throw new Error(`Phase "${phase.name}" is a repair phase and must be declared by a phase that emits a verdict.`);
|
|
240
|
+
throw new Error(`Phase "${phase.name}" is a repair phase and must be declared by a phase that emits a verdict or gate repair transition.`);
|
|
237
241
|
}
|
|
238
242
|
if (!consumedRepairs.has(index)) result.push(phase);
|
|
239
243
|
}
|
|
@@ -356,6 +360,7 @@ export async function loadRunsDir(cwd, configPath) {
|
|
|
356
360
|
return runsDir;
|
|
357
361
|
}
|
|
358
362
|
|
|
363
|
+
/** @returns {Promise<Config>} */
|
|
359
364
|
export async function loadConfig(cwd, overrides = {}, configPath) {
|
|
360
365
|
const configured = await importConfigFile(cwd, configPath);
|
|
361
366
|
const merged = {
|