@syntax-syllogism/aloop 0.7.0 → 0.8.1
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 +21 -7
- 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/src/types.d.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/** Shared static contracts for the ESM implementation. These declarations have no runtime output. */
|
|
2
|
+
|
|
3
|
+
export type Permission = string;
|
|
4
|
+
export type PhaseKind = 'agent' | 'gate' | 'publish';
|
|
5
|
+
export type PhaseRole = 'verdict' | 'repair';
|
|
6
|
+
|
|
7
|
+
export interface RetryPolicy {
|
|
8
|
+
maxAttempts: number;
|
|
9
|
+
maxRounds?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HermeticConfig {
|
|
13
|
+
runtime?: string;
|
|
14
|
+
image?: string | null;
|
|
15
|
+
network?: string[];
|
|
16
|
+
env?: string[];
|
|
17
|
+
secrets?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PhaseBase {
|
|
21
|
+
name: string;
|
|
22
|
+
kind: PhaseKind;
|
|
23
|
+
inputs: string[];
|
|
24
|
+
outputs: string[];
|
|
25
|
+
postconditions: string[];
|
|
26
|
+
permissions: Permission[];
|
|
27
|
+
optional: boolean;
|
|
28
|
+
requiresCleanTree?: boolean;
|
|
29
|
+
retry: RetryPolicy;
|
|
30
|
+
maxRounds?: number;
|
|
31
|
+
repair?: PhaseDescriptor[];
|
|
32
|
+
recheck?: boolean;
|
|
33
|
+
verdict?: boolean;
|
|
34
|
+
role?: PhaseRole;
|
|
35
|
+
hermetic?: HermeticConfig | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentPhase extends PhaseBase {
|
|
39
|
+
kind: 'agent';
|
|
40
|
+
prompt: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface GatePhase extends PhaseBase {
|
|
44
|
+
kind: 'gate';
|
|
45
|
+
commands?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PublishPhase extends PhaseBase {
|
|
49
|
+
kind: 'publish';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type PhaseDescriptor = AgentPhase | GatePhase | PublishPhase;
|
|
53
|
+
|
|
54
|
+
export interface Agent {
|
|
55
|
+
name: string;
|
|
56
|
+
model?: string;
|
|
57
|
+
effort?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AdapterCommandOptions {
|
|
61
|
+
prompt: string;
|
|
62
|
+
cwd: string;
|
|
63
|
+
addDirs: string[];
|
|
64
|
+
permissions?: Permission[];
|
|
65
|
+
timeoutMs?: number;
|
|
66
|
+
artifactOnly?: boolean;
|
|
67
|
+
agent?: Partial<Agent>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AdapterCommand {
|
|
71
|
+
command: string;
|
|
72
|
+
args: string[];
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface Adapter {
|
|
77
|
+
name?: string;
|
|
78
|
+
efforts?: string[];
|
|
79
|
+
command(options: AdapterCommandOptions): AdapterCommand;
|
|
80
|
+
version?: (options?: { agent?: Engine; cwd?: string }) => Promise<string | undefined>;
|
|
81
|
+
usage?: () => Usage | undefined;
|
|
82
|
+
createRenderer?: () => Renderer;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface Renderer {
|
|
86
|
+
write(text: string): string;
|
|
87
|
+
end(): string;
|
|
88
|
+
usage?: () => Usage | undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface Engine extends Agent {}
|
|
92
|
+
|
|
93
|
+
export interface Budget {
|
|
94
|
+
tokens?: number;
|
|
95
|
+
usd?: number;
|
|
96
|
+
wallClockMs?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface Config {
|
|
100
|
+
baseBranch: string;
|
|
101
|
+
branchPrefix: string;
|
|
102
|
+
remote: string | null;
|
|
103
|
+
adapters: Record<string, Adapter>;
|
|
104
|
+
engines: Record<string, Engine>;
|
|
105
|
+
phases: Array<string | Partial<PhaseDescriptor>>;
|
|
106
|
+
resolvedPhases: PhaseDescriptor[];
|
|
107
|
+
publish: { backend?: string | object; draft?: boolean };
|
|
108
|
+
gate: string[];
|
|
109
|
+
setup: string[];
|
|
110
|
+
shell: string;
|
|
111
|
+
maxRounds: number;
|
|
112
|
+
timeoutMs: number;
|
|
113
|
+
budget: Budget;
|
|
114
|
+
worktrees: boolean;
|
|
115
|
+
worktreeRoot: string | null;
|
|
116
|
+
promptDir: string;
|
|
117
|
+
runsDir: string;
|
|
118
|
+
hermetic: HermeticConfig;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface Finding {
|
|
122
|
+
issue?: string;
|
|
123
|
+
summary?: string;
|
|
124
|
+
file?: string;
|
|
125
|
+
line?: number;
|
|
126
|
+
[key: string]: any;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface Verdict {
|
|
130
|
+
verdict: 'APPROVED' | 'CHANGES_REQUESTED';
|
|
131
|
+
blocking: Finding[];
|
|
132
|
+
nits: Finding[];
|
|
133
|
+
summary: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface Usage {
|
|
137
|
+
tokens?: number;
|
|
138
|
+
cost?: number;
|
|
139
|
+
inputTokens?: number;
|
|
140
|
+
outputTokens?: number;
|
|
141
|
+
totalTokens?: number;
|
|
142
|
+
costUsd?: number;
|
|
143
|
+
[key: string]: unknown;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ManifestEntry {
|
|
147
|
+
phase: string;
|
|
148
|
+
status?: 'completed' | 'failed' | 'skipped' | 'stalled';
|
|
149
|
+
startedAt?: string;
|
|
150
|
+
completedAt?: string;
|
|
151
|
+
inputSha?: string;
|
|
152
|
+
outputSha?: string;
|
|
153
|
+
approvedSha?: string;
|
|
154
|
+
usage?: Usage;
|
|
155
|
+
[key: string]: unknown;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface RunData {
|
|
159
|
+
schemaVersion: number;
|
|
160
|
+
runId: string;
|
|
161
|
+
completed: string[];
|
|
162
|
+
rounds: Record<string, number>;
|
|
163
|
+
reviewedShas: Record<string, string>;
|
|
164
|
+
phases?: Record<string, Record<string, any>>;
|
|
165
|
+
pendingRepairs?: Record<string, any>;
|
|
166
|
+
[key: string]: any;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface RunState {
|
|
170
|
+
dir: string;
|
|
171
|
+
data: RunData;
|
|
172
|
+
readOnly: boolean;
|
|
173
|
+
manifest: { entries: ManifestEntry[] };
|
|
174
|
+
manifestMetadata: Record<string, any>;
|
|
175
|
+
hasSnapshot(): Promise<boolean>;
|
|
176
|
+
saveSnapshot(snapshot: Record<string, unknown>): Promise<void>;
|
|
177
|
+
logPath(name: string): string;
|
|
178
|
+
verdictPath(round: number): string;
|
|
179
|
+
isComplete(name: string): boolean;
|
|
180
|
+
markComplete(name: string, details?: Record<string, unknown>): Promise<void>;
|
|
181
|
+
record(patch: Record<string, unknown>): Promise<void>;
|
|
182
|
+
saveManifest(manifest: Record<string, unknown>): Promise<void>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface Summary {
|
|
186
|
+
phases: Array<Record<string, unknown>>;
|
|
187
|
+
stalled?: { phase: string; reason: string; [key: string]: any } | null;
|
|
188
|
+
branch?: string;
|
|
189
|
+
baseBranch?: string;
|
|
190
|
+
remote?: string;
|
|
191
|
+
worktree?: string;
|
|
192
|
+
runDir?: string;
|
|
193
|
+
task?: string;
|
|
194
|
+
taskFile?: string | null;
|
|
195
|
+
prUrl?: string | null;
|
|
196
|
+
pullRequest?: Record<string, unknown> | null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface Operations {
|
|
200
|
+
budgetStatus(ctx: unknown): any;
|
|
201
|
+
markBudgetExhaustedComplete(state: RunState, phase: PhaseDescriptor, details?: Record<string, unknown>): Promise<void>;
|
|
202
|
+
manifestEntry(phase: PhaseDescriptor, ctx: unknown, values?: Partial<ManifestEntry>): ManifestEntry;
|
|
203
|
+
markStalledManifest(ctx: unknown, phase: PhaseDescriptor, result: unknown, startIndex: number): Promise<void>;
|
|
204
|
+
recordBudgetStall(ctx: unknown, phase: PhaseDescriptor, budget: unknown): Promise<Summary['stalled']>;
|
|
205
|
+
recordManifest(ctx: unknown, entry: ManifestEntry): Promise<void>;
|
|
206
|
+
runAgent(phase: AgentPhase, ctx: unknown, variables: Record<string, unknown>): Promise<any>;
|
|
207
|
+
runGate(phase: GatePhase, ctx: unknown): Promise<any>;
|
|
208
|
+
runPublish(phase: PublishPhase, ctx: unknown, values: any): Promise<any>;
|
|
209
|
+
withRetries(phase: PhaseDescriptor, operation: () => Promise<any>, options?: Record<string, unknown>): Promise<any>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
declare global {
|
|
213
|
+
interface Error {
|
|
214
|
+
code?: string | number;
|
|
215
|
+
command?: unknown;
|
|
216
|
+
stdout?: string;
|
|
217
|
+
stderr?: string;
|
|
218
|
+
output?: string;
|
|
219
|
+
signal?: string | null;
|
|
220
|
+
timedOut?: boolean;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
declare module 'node:events' {
|
|
225
|
+
interface EventEmitter {
|
|
226
|
+
pid?: number;
|
|
227
|
+
stdout?: any;
|
|
228
|
+
stderr?: any;
|
|
229
|
+
stdin?: any;
|
|
230
|
+
directlyKilled?: boolean;
|
|
231
|
+
kill?: (...args: any[]) => any;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
declare module 'node:stream' {
|
|
236
|
+
interface Readable { isTTY?: boolean; }
|
|
237
|
+
interface PassThrough { isTTY?: boolean; }
|
|
238
|
+
}
|
package/src/verdict.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** @typedef {import('./types.js').Finding} Finding */
|
|
2
|
+
/** @typedef {import('./types.js').Verdict} Verdict */
|
|
3
|
+
|
|
1
4
|
import { readFile } from 'node:fs/promises';
|
|
2
5
|
|
|
3
6
|
export const APPROVED = 'APPROVED';
|
|
@@ -19,6 +22,7 @@ function extractJson(text) {
|
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
/** @param {Finding[]} list */
|
|
22
26
|
function validateFindings(list, label) {
|
|
23
27
|
list.forEach((finding, index) => {
|
|
24
28
|
const prefix = `${label}[${index}] is malformed:`;
|
|
@@ -39,6 +43,7 @@ function validateFindings(list, label) {
|
|
|
39
43
|
});
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
/** @returns {Finding[]} */
|
|
42
47
|
function parseFindingList(value, label) {
|
|
43
48
|
if (value === undefined) return [];
|
|
44
49
|
if (!Array.isArray(value)) {
|
|
@@ -55,9 +60,10 @@ function parseFindingList(value, label) {
|
|
|
55
60
|
* approval ships unreviewed code, and defaulting to changes burns rounds
|
|
56
61
|
* against a reviewer that is not actually reporting.
|
|
57
62
|
*/
|
|
63
|
+
/** @returns {Verdict} */
|
|
58
64
|
export function parseVerdict(text) {
|
|
59
65
|
const data = extractJson(text);
|
|
60
|
-
const verdict = String(data.verdict ?? '').toUpperCase();
|
|
66
|
+
const verdict = /** @type {Verdict['verdict']} */ (String(data.verdict ?? '').toUpperCase());
|
|
61
67
|
if (![APPROVED, CHANGES_REQUESTED].includes(verdict)) {
|
|
62
68
|
throw new Error(`Verdict must be ${APPROVED} or ${CHANGES_REQUESTED}, got ${JSON.stringify(data.verdict)}.`);
|
|
63
69
|
}
|
|
@@ -79,6 +85,7 @@ export function parseVerdict(text) {
|
|
|
79
85
|
};
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
/** @returns {Promise<Verdict>} */
|
|
82
89
|
export async function readVerdict(path) {
|
|
83
90
|
let text;
|
|
84
91
|
try {
|
|
@@ -92,6 +99,7 @@ export async function readVerdict(path) {
|
|
|
92
99
|
return parseVerdict(text);
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
/** @param {Finding[]} findings */
|
|
95
103
|
export function formatFindings(findings) {
|
|
96
104
|
if (!findings.length) return '(none)';
|
|
97
105
|
return findings
|