@syntax-syllogism/aloop 0.8.2 → 0.8.4
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 +9 -0
- package/README.md +6 -6
- package/bin/loop.mjs +6 -0
- package/package.json +1 -1
- package/src/adapters.mjs +12 -2
- package/src/pipeline.mjs +46 -10
- package/src/runner.mjs +73 -13
- package/src/state.mjs +28 -1
- package/src/types.d.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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
|
+
## [0.8.4] - 2026-09-20
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Nested repair baselines are now properly cleared when rerunning reviews
|
|
13
|
+
- Resume commit checks are now correctly skipped during dry runs
|
|
14
|
+
- Resume of existing commits now works correctly in read-only working directories
|
|
15
|
+
- Model and engine flags are now handled correctly
|
|
16
|
+
|
|
8
17
|
## [0.8.0] - 2026-09-20
|
|
9
18
|
|
|
10
19
|
### Added
|
package/README.md
CHANGED
|
@@ -113,12 +113,12 @@ export default { publish: { backend: gitlabBackend({ transport: myMcpTransport }
|
|
|
113
113
|
### Bring your own engine
|
|
114
114
|
|
|
115
115
|
Register an adapter under `adapters` when another CLI should run a phase. Its
|
|
116
|
-
`command({ prompt, cwd, addDirs,
|
|
117
|
-
`
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
accordingly; the adapter must translate `permissions` into its own
|
|
121
|
-
or write-capable invocation flags. It may also provide `efforts`
|
|
116
|
+
`command({ prompt, cwd, addDirs, permissions, artifactOnly, agent })` function
|
|
117
|
+
returns `{ command, args }`. For a read-only phase, `cwd` is a disposable
|
|
118
|
+
read-only source snapshot of the saved worktree and `addDirs` contains only
|
|
119
|
+
writable artifact roots. The runner supplies a permission level and restricts
|
|
120
|
+
`addDirs` accordingly; the adapter must translate `permissions` into its own
|
|
121
|
+
read-only or write-capable invocation flags. It may also provide `efforts`
|
|
122
122
|
validation and `createRenderer()` for streaming output. Custom adapter flags
|
|
123
123
|
are vendor-specific and are not verified by aloop. See the [phase permission
|
|
124
124
|
contract](docs/loop.md#phase-permissions) before enabling unattended runs.
|
package/bin/loop.mjs
CHANGED
|
@@ -32,6 +32,8 @@ export function parseLoopArgs(argv) {
|
|
|
32
32
|
branch: { type: 'string', short: 'b' },
|
|
33
33
|
'base-branch': { type: 'string' },
|
|
34
34
|
engine: { type: 'string', short: 'e' },
|
|
35
|
+
model: { type: 'string', short: 'm' },
|
|
36
|
+
effort: { type: 'string' },
|
|
35
37
|
'override-engine': { type: 'boolean' },
|
|
36
38
|
config: { type: 'string' },
|
|
37
39
|
phases: { type: 'string' },
|
|
@@ -79,6 +81,8 @@ export function parseLoopArgs(argv) {
|
|
|
79
81
|
branch: values.branch,
|
|
80
82
|
baseBranch: values['base-branch'],
|
|
81
83
|
engine: values.engine,
|
|
84
|
+
model: values.model,
|
|
85
|
+
effort: values.effort,
|
|
82
86
|
overrideEngine: values['override-engine'],
|
|
83
87
|
config: values.config,
|
|
84
88
|
phases: values.phases ? values.phases.split(',').map((phase) => phase.trim()).filter(Boolean) : undefined,
|
|
@@ -132,6 +136,8 @@ export function usage() {
|
|
|
132
136
|
' -b, --branch <name> Branch to build on (default: <branchPrefix><name>)',
|
|
133
137
|
' --base-branch <branch> Base to branch from and PR against (default: baseBranch config)',
|
|
134
138
|
' -e, --engine <name> Default engine: claude | codex | agy | gemini',
|
|
139
|
+
' -m, --model <name> Model for the default engine (overrides config; switching engines without it uses the new engine default)',
|
|
140
|
+
' --effort <level> Reasoning effort for the default engine (overrides config; ignored by engines without effort)',
|
|
135
141
|
' --override-engine With --resume and --engine, replace saved agent executables',
|
|
136
142
|
' --config <path> Load loop configuration from this file (also overrides saved config on resume)',
|
|
137
143
|
' --phases a,b,c Override the configured phase list',
|
package/package.json
CHANGED
package/src/adapters.mjs
CHANGED
|
@@ -413,16 +413,26 @@ const geminiAdapter = {
|
|
|
413
413
|
// until the process exits, so a multi-minute phase is indistinguishable from
|
|
414
414
|
// a hang. stream-json emits an event per step, which `createRenderer` turns
|
|
415
415
|
// back into readable lines — same reasoning as the claude adapter above.
|
|
416
|
+
//
|
|
417
|
+
// The prompt goes on stdin, not `--prompt`. Gemini's Windows launcher is a
|
|
418
|
+
// `.cmd`/`.ps1` shim, so on Git Bash aloop runs it through `cmd.exe /c`
|
|
419
|
+
// (see command.mjs windowsSpawnSpec). cmd.exe caps the whole command line at
|
|
420
|
+
// ~8191 chars and mangles newlines and metacharacters (`% ! & | < >`), so a
|
|
421
|
+
// real review prompt — work item plus instructions — arrives truncated or
|
|
422
|
+
// garbled and Gemini "ignores" it. stdin is a pipe cmd.exe never parses, so
|
|
423
|
+
// the prompt survives intact. `-p ""` still selects headless mode; Gemini
|
|
424
|
+
// documents `--prompt` as "appended to input on stdin", so an empty flag
|
|
425
|
+
// leaves the stdin prompt as the whole prompt.
|
|
416
426
|
const canWrite = permissionLevel(permissions) === PERMISSIONS.WRITE_WORKTREE || artifactOnly;
|
|
417
427
|
const args = [
|
|
418
|
-
'--prompt',
|
|
428
|
+
'--prompt', '',
|
|
419
429
|
'--approval-mode', canWrite ? 'yolo' : 'plan',
|
|
420
430
|
'--skip-trust',
|
|
421
431
|
'--output-format', 'stream-json',
|
|
422
432
|
];
|
|
423
433
|
if (agent.model) args.push('--model', agent.model);
|
|
424
434
|
for (const dir of addDirs) args.push('--include-directories', dir);
|
|
425
|
-
return { command: 'gemini', args };
|
|
435
|
+
return { command: 'gemini', args, input: prompt };
|
|
426
436
|
},
|
|
427
437
|
createRenderer: () => createJsonlRenderer(renderGeminiEvent),
|
|
428
438
|
};
|
package/src/pipeline.mjs
CHANGED
|
@@ -44,12 +44,23 @@ export function buildTaskContext({ task, taskFile }) {
|
|
|
44
44
|
return '';
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function overrideSavedEngines(agentSettings, engine, customAdapters) {
|
|
47
|
+
function overrideSavedEngines(agentSettings, { engine, model, effort }, customAdapters) {
|
|
48
|
+
// Switching engine drops the saved model/effort: they are vendor-specific and
|
|
49
|
+
// meaningless to the new engine (see the --engine override above). --model and
|
|
50
|
+
// --effort set them explicitly for the new engine; without them each phase
|
|
51
|
+
// falls back to the target engine's default. validateAgent rejects an effort
|
|
52
|
+
// the target engine cannot honor.
|
|
53
|
+
const target = validateAgent(
|
|
54
|
+
{ name: engine, ...(model ? { model } : {}), ...(effort ? { effort } : {}) },
|
|
55
|
+
customAdapters,
|
|
56
|
+
);
|
|
48
57
|
const overrides = {};
|
|
49
58
|
const overriddenSettings = Object.fromEntries(
|
|
50
59
|
Object.entries(agentSettings).map(([phaseName, agent]) => {
|
|
51
|
-
if (agent.name ===
|
|
52
|
-
|
|
60
|
+
if (agent.name === target.name && agent.model === target.model && agent.effort === target.effort) {
|
|
61
|
+
return [phaseName, agent];
|
|
62
|
+
}
|
|
63
|
+
const overridden = { ...target };
|
|
53
64
|
overrides[phaseName] = { from: agent, to: overridden };
|
|
54
65
|
return [phaseName, overridden];
|
|
55
66
|
}),
|
|
@@ -482,16 +493,16 @@ async function runAgent(phase, ctx, variables) {
|
|
|
482
493
|
log(` engine: ${agent.name}${agent.model ? ` model: ${agent.model}` : ''}${agent.effort ? ` effort: ${agent.effort}` : ''} prompt: ${path}`);
|
|
483
494
|
if (engineOverride) log(` resumed override: ${describeAgent(engineOverride.from)} → ${describeAgent(engineOverride.to)}`);
|
|
484
495
|
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
// cannot reach the real worktree
|
|
488
|
-
const agentCwd = artifactOnly ? ctx.state.dir : ctx.worktree;
|
|
496
|
+
// Read-only agents inspect the disposable source snapshot. Their writable
|
|
497
|
+
// added directories still point only at the run artifacts (and task file for
|
|
498
|
+
// review), so changing cwd cannot reach the real worktree.
|
|
499
|
+
const agentCwd = sourceSnapshot?.path ?? (artifactOnly ? ctx.state.dir : ctx.worktree);
|
|
489
500
|
// Only the verdict phase's prompt is instructed to edit the task file (its
|
|
490
501
|
// Code Review section), so only it is granted the external task-file repo as
|
|
491
502
|
// writable. Every other artifact-only phase — pr-description included — gets
|
|
492
503
|
// just its own run directory; it has no business touching the task file repo.
|
|
493
504
|
const artifactOnlyDirs = phase.role === 'verdict' ? ctx.artifactDirs : [ctx.state.dir];
|
|
494
|
-
const { command, args } = adapter.command({
|
|
505
|
+
const { command, args, input } = adapter.command({
|
|
495
506
|
prompt,
|
|
496
507
|
cwd: agentCwd,
|
|
497
508
|
addDirs: worktreeWrite ? ctx.addDirs : [...artifactOnlyDirs, ...(sourceSnapshot ? [sourceSnapshot.path] : [])],
|
|
@@ -539,6 +550,10 @@ async function runAgent(phase, ctx, variables) {
|
|
|
539
550
|
env: execution.env,
|
|
540
551
|
timeoutMs: ctx.config.timeoutMs,
|
|
541
552
|
activeProcessPath: ctx.activeProcessPath,
|
|
553
|
+
// Adapters that carry the prompt on stdin (gemini, to dodge the cmd.exe
|
|
554
|
+
// command-line limit on Windows) return it as `input`; a hermetic wrapper
|
|
555
|
+
// forwards stdin to the sandboxed process unchanged.
|
|
556
|
+
input,
|
|
542
557
|
onOutput: (text, stream) => emit(stream === 'stderr' ? text : renderer.write(text)),
|
|
543
558
|
});
|
|
544
559
|
} catch (error) {
|
|
@@ -607,7 +622,24 @@ export async function runLoop(options = {}) {
|
|
|
607
622
|
...(args.noWorktree ? { worktrees: false } : {}),
|
|
608
623
|
}, args.config ? resolve(cwd, args.config) : undefined);
|
|
609
624
|
if (args.engine && !args.overrideEngine) {
|
|
610
|
-
|
|
625
|
+
// A model/effort string is vendor-specific — `gpt-5.6-terra` means nothing
|
|
626
|
+
// to gemini — so switching to a different engine must not inherit the
|
|
627
|
+
// previous engine's model/effort. Carry them over only when the engine is
|
|
628
|
+
// unchanged; --model/--effort below can still set them explicitly.
|
|
629
|
+
const base = args.engine === config.engines.default.name
|
|
630
|
+
? config.engines.default
|
|
631
|
+
: { name: args.engine };
|
|
632
|
+
config.engines.default = validateAgent({ ...base, name: args.engine }, config.adapters);
|
|
633
|
+
}
|
|
634
|
+
if ((args.model || args.effort) && !args.overrideEngine) {
|
|
635
|
+
config.engines.default = validateAgent(
|
|
636
|
+
{
|
|
637
|
+
...config.engines.default,
|
|
638
|
+
...(args.model ? { model: args.model } : {}),
|
|
639
|
+
...(args.effort ? { effort: args.effort } : {}),
|
|
640
|
+
},
|
|
641
|
+
config.adapters,
|
|
642
|
+
);
|
|
611
643
|
}
|
|
612
644
|
if (args.overrideEngine && !args.resume) {
|
|
613
645
|
throw new Error('--override-engine requires --resume.');
|
|
@@ -731,7 +763,11 @@ export async function runLoop(options = {}) {
|
|
|
731
763
|
if (!savedAgentSettings) {
|
|
732
764
|
throw new Error('--override-engine requires saved agent settings from an earlier run.');
|
|
733
765
|
}
|
|
734
|
-
({ agentSettings, overrides: engineOverrides } = overrideSavedEngines(
|
|
766
|
+
({ agentSettings, overrides: engineOverrides } = overrideSavedEngines(
|
|
767
|
+
savedAgentSettings,
|
|
768
|
+
{ engine: args.engine, model: args.model, effort: args.effort },
|
|
769
|
+
config.adapters,
|
|
770
|
+
));
|
|
735
771
|
if (Object.keys(engineOverrides).length > 0) {
|
|
736
772
|
const engineOverride = {
|
|
737
773
|
at: new Date().toISOString(),
|
package/src/runner.mjs
CHANGED
|
@@ -57,6 +57,19 @@ async function clearPendingRepair(phase, state) {
|
|
|
57
57
|
await state.record({ pendingRepairs });
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function requiresCommitBaseline(phase) {
|
|
61
|
+
return hasPostcondition(phase, 'head-advanced') || hasPostcondition(phase, 'head-advanced-or-rebuttal');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function phaseHeadBefore(phase, ctx) {
|
|
65
|
+
if (ctx.dryRun) return null;
|
|
66
|
+
if (requiresCommitBaseline(phase)) {
|
|
67
|
+
return ctx.state.baselineFor(phase.name, () => ctx.git.revParse());
|
|
68
|
+
}
|
|
69
|
+
if (hasPostcondition(phase, 'head-unchanged')) return ctx.git.revParse();
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
async function invalidateGateCompletion(state, reviewedSha) {
|
|
61
74
|
const gate = state.manifest.entries.findLast((entry) => entry.role === 'gate'
|
|
62
75
|
&& entry.status === 'completed'
|
|
@@ -65,25 +78,32 @@ async function invalidateGateCompletion(state, reviewedSha) {
|
|
|
65
78
|
await state.record({ completed: state.data.completed.filter((name) => name !== gate.phase) });
|
|
66
79
|
}
|
|
67
80
|
|
|
81
|
+
function phaseAndRepairNames(phase) {
|
|
82
|
+
return [phase.name, ...(phase.repair ?? []).flatMap(phaseAndRepairNames)];
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
async function invalidateFollowingPhaseState(state, phases, phaseName) {
|
|
69
86
|
const phaseIndex = phases.findIndex((phase) => phase.name === phaseName);
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
if (phaseIndex < 0) return;
|
|
88
|
+
const invalidatedNames = new Set(phases.slice(phaseIndex).flatMap(phaseAndRepairNames));
|
|
72
89
|
|
|
73
|
-
const completed = state.data.completed.filter((name) => !
|
|
90
|
+
const completed = state.data.completed.filter((name) => !invalidatedNames.has(name));
|
|
74
91
|
const phaseState = Object.fromEntries(
|
|
75
|
-
Object.entries(state.data.phases ?? {}).filter(([name]) => !
|
|
92
|
+
Object.entries(state.data.phases ?? {}).filter(([name]) => !invalidatedNames.has(name)),
|
|
76
93
|
);
|
|
77
94
|
const pendingRepairs = Object.fromEntries(
|
|
78
|
-
Object.entries(state.data.pendingRepairs ?? {}).filter(([name]) => !
|
|
95
|
+
Object.entries(state.data.pendingRepairs ?? {}).filter(([name]) => !invalidatedNames.has(name)),
|
|
79
96
|
);
|
|
80
97
|
const rounds = Object.fromEntries(
|
|
81
|
-
Object.entries(state.data.rounds ?? {}).filter(([name]) => !
|
|
98
|
+
Object.entries(state.data.rounds ?? {}).filter(([name]) => !invalidatedNames.has(name)),
|
|
82
99
|
);
|
|
83
100
|
const reviewedShas = Object.fromEntries(
|
|
84
|
-
Object.entries(state.data.reviewedShas ?? {}).filter(([name]) => !
|
|
101
|
+
Object.entries(state.data.reviewedShas ?? {}).filter(([name]) => !invalidatedNames.has(name)),
|
|
102
|
+
);
|
|
103
|
+
const phaseBaselines = Object.fromEntries(
|
|
104
|
+
Object.entries(state.data.phaseBaselines ?? {}).filter(([name]) => !invalidatedNames.has(name)),
|
|
85
105
|
);
|
|
86
|
-
await state.record({ completed, phases: phaseState, pendingRepairs, rounds, reviewedShas });
|
|
106
|
+
await state.record({ completed, phases: phaseState, pendingRepairs, rounds, reviewedShas, phaseBaselines });
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
async function runRepairs(phase, ctx, baseVariables, pending, operations, { retainCheckpoint = false } = {}) {
|
|
@@ -131,9 +151,35 @@ async function runRepairs(phase, ctx, baseVariables, pending, operations, { reta
|
|
|
131
151
|
}));
|
|
132
152
|
}
|
|
133
153
|
} else {
|
|
134
|
-
const headBefore =
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
const headBefore = await phaseHeadBefore(repair, ctx);
|
|
155
|
+
const forceNotedRepair = ctx.resume && ctx.note?.targetPhase === phase.name;
|
|
156
|
+
if (!ctx.dryRun && ctx.resume && requiresCommitBaseline(repair) && !forceNotedRepair) {
|
|
157
|
+
const stalledReason = await codePhasePostcondition(repair, ctx, {
|
|
158
|
+
hasFindings: pending.verdict?.blocking?.length > 0,
|
|
159
|
+
headBefore,
|
|
160
|
+
round: pending.round,
|
|
161
|
+
});
|
|
162
|
+
if (!stalledReason) {
|
|
163
|
+
const outputSha = await ctx.git.revParse();
|
|
164
|
+
const responsePath = join(ctx.state.dir, `response-round-${pending.round}.md`);
|
|
165
|
+
await recordManifest(ctx, manifestEntry(repair, ctx, {
|
|
166
|
+
inputSha: headBefore,
|
|
167
|
+
outputSha,
|
|
168
|
+
round: pending.round,
|
|
169
|
+
artifacts: [
|
|
170
|
+
...(await fileExists(responsePath) ? [`response-round-${pending.round}.md`] : []),
|
|
171
|
+
],
|
|
172
|
+
status: 'completed',
|
|
173
|
+
}));
|
|
174
|
+
await ctx.state.record({
|
|
175
|
+
pendingRepairs: {
|
|
176
|
+
...ctx.state.data.pendingRepairs,
|
|
177
|
+
[phase.name]: { ...pending, nextRepair: index + 1 },
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
137
183
|
const repairCtx = ctx.note?.targetPhase === phase.name
|
|
138
184
|
? { ...ctx, note: { ...ctx.note, repairPhase: repair.name } }
|
|
139
185
|
: ctx;
|
|
@@ -616,8 +662,22 @@ export async function runPhases({
|
|
|
616
662
|
await state.markComplete(phase.name, { rounds: result.rounds });
|
|
617
663
|
continue;
|
|
618
664
|
}
|
|
619
|
-
const headBefore =
|
|
620
|
-
|
|
665
|
+
const headBefore = await phaseHeadBefore(phase, ctx);
|
|
666
|
+
if (!ctx.dryRun && args.resume && requiresCommitBaseline(phase) && !forceNotedPhase) {
|
|
667
|
+
const stalledReason = await codePhasePostcondition(phase, ctx, { headBefore });
|
|
668
|
+
if (!stalledReason) {
|
|
669
|
+
const outputSha = await ctx.git.revParse();
|
|
670
|
+
log(` ${phase.name} already satisfies its commit postcondition; skipping agent on resume`);
|
|
671
|
+
await recordManifest(ctx, manifestEntry(phase, ctx, {
|
|
672
|
+
inputSha: headBefore,
|
|
673
|
+
outputSha,
|
|
674
|
+
status: 'completed',
|
|
675
|
+
}));
|
|
676
|
+
await state.markComplete(phase.name);
|
|
677
|
+
summary.phases.push({ name: phase.name, ok: true });
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
621
681
|
const result = await withRetries(phase, () => runAgent(phase, ctx, variables));
|
|
622
682
|
const stalledReason = await codePhasePostcondition(phase, ctx, { headBefore });
|
|
623
683
|
if (stalledReason) {
|
package/src/state.mjs
CHANGED
|
@@ -37,6 +37,15 @@ function isActiveProcessAlive(activeProcess) {
|
|
|
37
37
|
return isProcessGroupAlive(activeProcess?.processGroupId) || isProcessAlive(activeProcess?.pid);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
async function pathExists(path) {
|
|
41
|
+
try {
|
|
42
|
+
await access(path);
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
async function readActiveProcess(dir) {
|
|
41
50
|
try {
|
|
42
51
|
return JSON.parse(await readFile(join(dir, 'active-command.json'), 'utf8'));
|
|
@@ -172,6 +181,7 @@ export class RunState {
|
|
|
172
181
|
completed: [],
|
|
173
182
|
rounds: {},
|
|
174
183
|
reviewedShas: {},
|
|
184
|
+
phaseBaselines: {},
|
|
175
185
|
...seed,
|
|
176
186
|
schemaVersion: STATE_SCHEMA_VERSION,
|
|
177
187
|
};
|
|
@@ -238,7 +248,16 @@ export class RunState {
|
|
|
238
248
|
await publishLock(path, lockToken, contents);
|
|
239
249
|
return lockToken;
|
|
240
250
|
} catch (error) {
|
|
241
|
-
|
|
251
|
+
// POSIX rejects a rename onto the existing (non-empty) lock directory
|
|
252
|
+
// with EEXIST/ENOTEMPTY — the signal that the lock is already held.
|
|
253
|
+
// Windows rejects the same rename with EPERM (sometimes EACCES)
|
|
254
|
+
// instead, so without this a `--resume` that finds any prior lock dir
|
|
255
|
+
// crashes with "EPERM: operation not permitted, rename". Treat those as
|
|
256
|
+
// contention too, but only when the lock path is actually present, so a
|
|
257
|
+
// genuine permission failure still surfaces instead of spinning here.
|
|
258
|
+
const contended = ['EEXIST', 'ENOTEMPTY'].includes(error.code)
|
|
259
|
+
|| (['EPERM', 'EACCES'].includes(error.code) && await pathExists(path));
|
|
260
|
+
if (!contended) throw error;
|
|
242
261
|
}
|
|
243
262
|
|
|
244
263
|
let existing;
|
|
@@ -368,6 +387,14 @@ export class RunState {
|
|
|
368
387
|
await this.save();
|
|
369
388
|
}
|
|
370
389
|
|
|
390
|
+
async baselineFor(name, compute) {
|
|
391
|
+
const phaseBaselines = this.data.phaseBaselines ?? {};
|
|
392
|
+
if (phaseBaselines[name]) return phaseBaselines[name];
|
|
393
|
+
const baseline = await compute();
|
|
394
|
+
await this.record({ phaseBaselines: { ...phaseBaselines, [name]: baseline } });
|
|
395
|
+
return baseline;
|
|
396
|
+
}
|
|
397
|
+
|
|
371
398
|
async record(patch) {
|
|
372
399
|
Object.assign(this.data, patch);
|
|
373
400
|
await this.save();
|
package/src/types.d.ts
CHANGED
|
@@ -70,6 +70,12 @@ export interface AdapterCommandOptions {
|
|
|
70
70
|
export interface AdapterCommand {
|
|
71
71
|
command: string;
|
|
72
72
|
args: string[];
|
|
73
|
+
/**
|
|
74
|
+
* Optional stdin for the command. Adapters use it to carry the prompt off the
|
|
75
|
+
* argument vector (gemini does, to dodge the cmd.exe command-line limit on
|
|
76
|
+
* Windows) rather than passing it as an argv element.
|
|
77
|
+
*/
|
|
78
|
+
input?: string;
|
|
73
79
|
[key: string]: unknown;
|
|
74
80
|
}
|
|
75
81
|
|
|
@@ -161,6 +167,7 @@ export interface RunData {
|
|
|
161
167
|
completed: string[];
|
|
162
168
|
rounds: Record<string, number>;
|
|
163
169
|
reviewedShas: Record<string, string>;
|
|
170
|
+
phaseBaselines?: Record<string, string>;
|
|
164
171
|
phases?: Record<string, Record<string, any>>;
|
|
165
172
|
pendingRepairs?: Record<string, any>;
|
|
166
173
|
[key: string]: any;
|
|
@@ -178,6 +185,7 @@ export interface RunState {
|
|
|
178
185
|
verdictPath(round: number): string;
|
|
179
186
|
isComplete(name: string): boolean;
|
|
180
187
|
markComplete(name: string, details?: Record<string, unknown>): Promise<void>;
|
|
188
|
+
baselineFor(name: string, compute: () => Promise<string>): Promise<string>;
|
|
181
189
|
record(patch: Record<string, unknown>): Promise<void>;
|
|
182
190
|
saveManifest(manifest: Record<string, unknown>): Promise<void>;
|
|
183
191
|
}
|