@fred-drake/anvil 0.0.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/README.md +72 -0
- package/assets/anvil-logo.png +0 -0
- package/assets/anvil-logo.txt +21 -0
- package/extensions/anvil/index.ts +1 -0
- package/package.json +58 -0
- package/skills/anvil-workflow-builder/SKILL.md +109 -0
- package/src/discovery.ts +149 -0
- package/src/engine.ts +591 -0
- package/src/errors.ts +18 -0
- package/src/gates.ts +198 -0
- package/src/index.ts +700 -0
- package/src/paths.ts +35 -0
- package/src/prompts.ts +253 -0
- package/src/shell.ts +3 -0
- package/src/subagent/child.ts +61 -0
- package/src/subagent/cmux.ts +227 -0
- package/src/subagent/exit.ts +113 -0
- package/src/subagent/herdr.ts +142 -0
- package/src/subagent/runner.ts +178 -0
- package/src/types.ts +122 -0
- package/src/ui.ts +94 -0
- package/src/validate.ts +340 -0
package/src/gates.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { EngineHost } from "./engine.ts";
|
|
2
|
+
import { abortError } from "./errors.ts";
|
|
3
|
+
import { buildAgentCheckInstruction, buildVerdictReprompt, renderCommandTemplatable } from "./prompts.ts";
|
|
4
|
+
import type { AgentCheck, Check, DeterministicCheck, WorkflowContext, WorkflowDefinition, WorkflowStep } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export interface Verdict {
|
|
7
|
+
checkId: string;
|
|
8
|
+
pass: boolean;
|
|
9
|
+
reason: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GateResult {
|
|
13
|
+
check: Check;
|
|
14
|
+
checkId: string;
|
|
15
|
+
name: string;
|
|
16
|
+
pass: boolean;
|
|
17
|
+
reason: string;
|
|
18
|
+
output?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_DETERMINISTIC_TIMEOUT_MS = 300_000;
|
|
22
|
+
export const DEFAULT_AGENT_VERDICT_TIMEOUT_MS = 300_000;
|
|
23
|
+
|
|
24
|
+
export class VerdictBus {
|
|
25
|
+
private waiters = new Map<
|
|
26
|
+
string,
|
|
27
|
+
{
|
|
28
|
+
resolve: (verdict: Verdict | undefined) => void;
|
|
29
|
+
reject: (error: Error) => void;
|
|
30
|
+
timeout: NodeJS.Timeout;
|
|
31
|
+
onAbort?: () => void;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
}
|
|
34
|
+
>();
|
|
35
|
+
|
|
36
|
+
awaitVerdict(checkId: string, timeoutMs: number, signal?: AbortSignal): Promise<Verdict | undefined> {
|
|
37
|
+
if (signal?.aborted) return Promise.reject(abortError());
|
|
38
|
+
|
|
39
|
+
const existing = this.waiters.get(checkId);
|
|
40
|
+
if (existing) {
|
|
41
|
+
clearTimeout(existing.timeout);
|
|
42
|
+
existing.signal?.removeEventListener("abort", existing.onAbort ?? (() => undefined));
|
|
43
|
+
existing.resolve(undefined);
|
|
44
|
+
this.waiters.delete(checkId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return new Promise<Verdict | undefined>((resolve, reject) => {
|
|
48
|
+
const cleanup = () => {
|
|
49
|
+
clearTimeout(timeout);
|
|
50
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
51
|
+
this.waiters.delete(checkId);
|
|
52
|
+
};
|
|
53
|
+
const timeout = setTimeout(() => {
|
|
54
|
+
cleanup();
|
|
55
|
+
resolve(undefined);
|
|
56
|
+
}, timeoutMs);
|
|
57
|
+
const onAbort = signal
|
|
58
|
+
? () => {
|
|
59
|
+
cleanup();
|
|
60
|
+
reject(abortError());
|
|
61
|
+
}
|
|
62
|
+
: undefined;
|
|
63
|
+
if (signal && onAbort) signal.addEventListener("abort", onAbort, { once: true });
|
|
64
|
+
this.waiters.set(checkId, { resolve, reject, timeout, onAbort, signal });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
reportVerdict(checkId: string, pass: boolean, reason: string): boolean {
|
|
69
|
+
const waiter = this.waiters.get(checkId);
|
|
70
|
+
if (!waiter) return false;
|
|
71
|
+
clearTimeout(waiter.timeout);
|
|
72
|
+
waiter.signal?.removeEventListener("abort", waiter.onAbort ?? (() => undefined));
|
|
73
|
+
this.waiters.delete(checkId);
|
|
74
|
+
waiter.resolve({ checkId, pass, reason });
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
clear(): void {
|
|
79
|
+
for (const [checkId, waiter] of this.waiters) {
|
|
80
|
+
clearTimeout(waiter.timeout);
|
|
81
|
+
waiter.signal?.removeEventListener("abort", waiter.onAbort ?? (() => undefined));
|
|
82
|
+
waiter.resolve(undefined);
|
|
83
|
+
this.waiters.delete(checkId);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function executeDeterministicCheck(args: {
|
|
89
|
+
host: EngineHost;
|
|
90
|
+
check: DeterministicCheck;
|
|
91
|
+
ctx: WorkflowContext;
|
|
92
|
+
checkId: string;
|
|
93
|
+
signal?: AbortSignal;
|
|
94
|
+
}): Promise<GateResult> {
|
|
95
|
+
const command = await renderCommandTemplatable(args.check.command, args.ctx);
|
|
96
|
+
const result = await args.host.exec("bash", ["-c", command], {
|
|
97
|
+
cwd: args.check.cwd ?? args.ctx.cwd,
|
|
98
|
+
timeout: args.check.timeoutMs ?? DEFAULT_DETERMINISTIC_TIMEOUT_MS,
|
|
99
|
+
signal: args.signal,
|
|
100
|
+
});
|
|
101
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
102
|
+
const pass = result.code === 0;
|
|
103
|
+
const timeoutMs = args.check.timeoutMs ?? DEFAULT_DETERMINISTIC_TIMEOUT_MS;
|
|
104
|
+
return {
|
|
105
|
+
check: args.check,
|
|
106
|
+
checkId: args.checkId,
|
|
107
|
+
name: checkDisplayName(args.check, args.checkId),
|
|
108
|
+
pass,
|
|
109
|
+
reason: pass
|
|
110
|
+
? "command exited 0"
|
|
111
|
+
: result.killed
|
|
112
|
+
? `command timed out after ${timeoutMs}ms${output ? `: ${tail(output, 2000)}` : ""}`
|
|
113
|
+
: tail(output || `command exited ${result.code}`, 2000),
|
|
114
|
+
output,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function executeAgentCheck(args: {
|
|
119
|
+
host: EngineHost;
|
|
120
|
+
workflow: WorkflowDefinition;
|
|
121
|
+
step: WorkflowStep;
|
|
122
|
+
check: AgentCheck;
|
|
123
|
+
ctx: WorkflowContext;
|
|
124
|
+
checkId: string;
|
|
125
|
+
signal?: AbortSignal;
|
|
126
|
+
timeoutMs?: number;
|
|
127
|
+
}): Promise<GateResult> {
|
|
128
|
+
let verdictPromise = args.host.awaitVerdict(
|
|
129
|
+
args.checkId,
|
|
130
|
+
args.timeoutMs ?? DEFAULT_AGENT_VERDICT_TIMEOUT_MS,
|
|
131
|
+
args.signal,
|
|
132
|
+
);
|
|
133
|
+
const instruction = await buildAgentCheckInstruction({
|
|
134
|
+
workflow: args.workflow,
|
|
135
|
+
step: args.step,
|
|
136
|
+
check: args.check,
|
|
137
|
+
ctx: args.ctx,
|
|
138
|
+
checkId: args.checkId,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
args.host.sendInstruction(instruction);
|
|
142
|
+
let turnPromise = args.host.waitForTurnComplete(args.signal);
|
|
143
|
+
const first = await raceVerdictOrTurn(verdictPromise, turnPromise);
|
|
144
|
+
if (first.kind === "verdict" && first.verdict) {
|
|
145
|
+
await turnPromise;
|
|
146
|
+
return verdictToGateResult(args.check, args.checkId, first.verdict);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
verdictPromise = args.host.awaitVerdict(
|
|
150
|
+
args.checkId,
|
|
151
|
+
args.timeoutMs ?? DEFAULT_AGENT_VERDICT_TIMEOUT_MS,
|
|
152
|
+
args.signal,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
args.host.sendInstruction(buildVerdictReprompt(args.checkId));
|
|
156
|
+
turnPromise = args.host.waitForTurnComplete(args.signal);
|
|
157
|
+
const second = await raceVerdictOrTurn(verdictPromise, turnPromise);
|
|
158
|
+
if (second.kind === "verdict" && second.verdict) {
|
|
159
|
+
await turnPromise;
|
|
160
|
+
return verdictToGateResult(args.check, args.checkId, second.verdict);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
check: args.check,
|
|
165
|
+
checkId: args.checkId,
|
|
166
|
+
name: checkDisplayName(args.check, args.checkId),
|
|
167
|
+
pass: false,
|
|
168
|
+
reason: "no verdict reported",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function verdictToGateResult(check: AgentCheck, checkId: string, verdict: Verdict): GateResult {
|
|
173
|
+
return {
|
|
174
|
+
check,
|
|
175
|
+
checkId,
|
|
176
|
+
name: checkDisplayName(check, checkId),
|
|
177
|
+
pass: verdict.pass,
|
|
178
|
+
reason: verdict.reason,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function raceVerdictOrTurn(
|
|
183
|
+
verdictPromise: Promise<Verdict | undefined>,
|
|
184
|
+
turnPromise: Promise<void>,
|
|
185
|
+
): Promise<{ kind: "verdict"; verdict: Verdict | undefined } | { kind: "turn" }> {
|
|
186
|
+
return Promise.race([
|
|
187
|
+
verdictPromise.then((verdict) => ({ kind: "verdict" as const, verdict })),
|
|
188
|
+
turnPromise.then(() => ({ kind: "turn" as const })),
|
|
189
|
+
]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function checkDisplayName(check: Check, fallback: string): string {
|
|
193
|
+
return check.name ?? check.id ?? fallback;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function tail(text: string, maxChars: number): string {
|
|
197
|
+
return text.length <= maxChars ? text : text.slice(-maxChars);
|
|
198
|
+
}
|