@loops-adk/core 0.1.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.
- package/LICENSE +21 -0
- package/README.md +486 -0
- package/bin/loops.mjs +16 -0
- package/dist/App-3YQS6DXA.js +461 -0
- package/dist/App-3YQS6DXA.js.map +1 -0
- package/dist/agent-sdk-RF5VJZAT.js +95 -0
- package/dist/agent-sdk-RF5VJZAT.js.map +1 -0
- package/dist/anthropic-api-XJY6Y4T2.js +131 -0
- package/dist/anthropic-api-XJY6Y4T2.js.map +1 -0
- package/dist/api.d.ts +949 -0
- package/dist/api.js +898 -0
- package/dist/api.js.map +1 -0
- package/dist/chunk-33YIGWNU.js +63 -0
- package/dist/chunk-33YIGWNU.js.map +1 -0
- package/dist/chunk-3BPU34DE.js +2163 -0
- package/dist/chunk-3BPU34DE.js.map +1 -0
- package/dist/chunk-CXEPZHSR.js +86 -0
- package/dist/chunk-CXEPZHSR.js.map +1 -0
- package/dist/chunk-I3STY7U6.js +61 -0
- package/dist/chunk-I3STY7U6.js.map +1 -0
- package/dist/chunk-JFTXJ7I2.js +18 -0
- package/dist/chunk-JFTXJ7I2.js.map +1 -0
- package/dist/chunk-XC46B4FD.js +9 -0
- package/dist/chunk-XC46B4FD.js.map +1 -0
- package/dist/chunk-Y2SD7GBL.js +30 -0
- package/dist/chunk-Y2SD7GBL.js.map +1 -0
- package/dist/claude-cli-U7WEVAOL.js +124 -0
- package/dist/claude-cli-U7WEVAOL.js.map +1 -0
- package/dist/codex-6I5UZ2HM.js +60 -0
- package/dist/codex-6I5UZ2HM.js.map +1 -0
- package/dist/env/command.d.ts +53 -0
- package/dist/env/command.js +3 -0
- package/dist/env/command.js.map +1 -0
- package/dist/env/docker.d.ts +38 -0
- package/dist/env/docker.js +33 -0
- package/dist/env/docker.js.map +1 -0
- package/dist/env/sst.d.ts +39 -0
- package/dist/env/sst.js +20 -0
- package/dist/env/sst.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +620 -0
- package/dist/index.js.map +1 -0
- package/dist/types-B4wGVpqo.d.ts +898 -0
- package/package.json +100 -0
- package/skills/author-loop/SKILL.md +121 -0
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pluggable execution backend. A `Step` asks an `Engine` to run one agent
|
|
3
|
+
* turn with a *fresh context* and stream events back. Each call is independent —
|
|
4
|
+
* that is what gives every loop iteration its clean slate.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Built-in, registry-resolvable adapter names. The union is open (`& {}` trick)
|
|
8
|
+
* so callers can name and register their own engines — the core never assumes a
|
|
9
|
+
* fixed provider set. (`mock` is constructed directly in tests/examples, not
|
|
10
|
+
* registered by name, so it is intentionally not listed here.)
|
|
11
|
+
*/
|
|
12
|
+
type EngineName = 'agent-sdk' | 'claude-cli' | 'anthropic-api' | (string & {});
|
|
13
|
+
interface Usage {
|
|
14
|
+
inputTokens: number;
|
|
15
|
+
outputTokens: number;
|
|
16
|
+
}
|
|
17
|
+
/** Tools an agent uses to spawn sub-agents / fan out. A `leaf` request disallows these. */
|
|
18
|
+
declare const SUBAGENT_TOOLS: string[];
|
|
19
|
+
interface AgentRequest {
|
|
20
|
+
prompt: string;
|
|
21
|
+
system?: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
maxTokens?: number;
|
|
24
|
+
/** Tool allowlist, where the backend supports tools (SDK / CLI). */
|
|
25
|
+
allowedTools?: string[];
|
|
26
|
+
cwd?: string;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Forbid this agent from spawning sub-agents (fanning out). A leaf agent is told to
|
|
30
|
+
* disallow the sub-agent tool (`SUBAGENT_TOOLS`), so a branch of the graph bottoms out
|
|
31
|
+
* here instead of expanding into an uncontrolled swarm — control over where work stops.
|
|
32
|
+
* Authoritative over `allowedTools` (a disallow wins). Engines with no sub-agent tool
|
|
33
|
+
* (anthropic-api, mock) ignore it.
|
|
34
|
+
*/
|
|
35
|
+
leaf?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface AgentResult {
|
|
38
|
+
/** Final assistant text (concatenated across blocks). */
|
|
39
|
+
text: string;
|
|
40
|
+
usage: Usage;
|
|
41
|
+
model: string;
|
|
42
|
+
stopReason?: string;
|
|
43
|
+
/** Backend-native final payload, for escape-hatch inspection. */
|
|
44
|
+
raw?: unknown;
|
|
45
|
+
}
|
|
46
|
+
/** Streamed during a run. The runtime re-tags these as `LoopEvent`s. */
|
|
47
|
+
type EngineStreamEvent = {
|
|
48
|
+
type: 'text';
|
|
49
|
+
delta: string;
|
|
50
|
+
} | {
|
|
51
|
+
type: 'thinking';
|
|
52
|
+
delta: string;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'tool';
|
|
55
|
+
name: string;
|
|
56
|
+
phase: 'use' | 'result';
|
|
57
|
+
} | {
|
|
58
|
+
type: 'usage';
|
|
59
|
+
usage: Usage;
|
|
60
|
+
model: string;
|
|
61
|
+
};
|
|
62
|
+
type EngineEventSink = (event: EngineStreamEvent) => void;
|
|
63
|
+
interface Engine {
|
|
64
|
+
readonly name: EngineName;
|
|
65
|
+
/**
|
|
66
|
+
* Run one fresh agent turn. Contract for the `usage` stream event: emit it
|
|
67
|
+
* **exactly once, at the end** of the turn — stats sums every `usage` event,
|
|
68
|
+
* so a backend that emits incremental usage mid-stream would inflate totals.
|
|
69
|
+
*/
|
|
70
|
+
run(req: AgentRequest, onEvent: EngineEventSink, signal: AbortSignal): Promise<AgentResult>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Anywhere an engine can be selected, accept either a registered name or a
|
|
74
|
+
* ready-made `Engine`. The latter is the "bring your own provider/framework"
|
|
75
|
+
* escape hatch — the runtime treats every backend through this one interface.
|
|
76
|
+
*/
|
|
77
|
+
type EngineRef = EngineName | Engine;
|
|
78
|
+
declare function isEngine(ref: EngineRef | undefined): ref is Engine;
|
|
79
|
+
/**
|
|
80
|
+
* How a tool-using engine (claude-cli / agent-sdk) treats permission prompts.
|
|
81
|
+
* Mirrors the Claude Code values. `bypassPermissions` lets a headless worker
|
|
82
|
+
* read/write/run without prompting — required for an unattended agent that must
|
|
83
|
+
* touch the filesystem or shell, and to be set deliberately.
|
|
84
|
+
*/
|
|
85
|
+
type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk' | 'auto';
|
|
86
|
+
/** Per-run options that the registry uses to construct engines. */
|
|
87
|
+
interface EngineOptions {
|
|
88
|
+
/** Default model when a request/step does not name one. */
|
|
89
|
+
defaultModel?: string;
|
|
90
|
+
apiKey?: string;
|
|
91
|
+
/** For `claude-cli`: path to the binary (defaults to `claude` on PATH). */
|
|
92
|
+
cliBinary?: string;
|
|
93
|
+
/** Extra args appended to the `claude` invocation. */
|
|
94
|
+
cliArgs?: string[];
|
|
95
|
+
/**
|
|
96
|
+
* Permission mode for tool-using engines (claude-cli `--permission-mode`,
|
|
97
|
+
* agent-sdk `permissionMode`). Unset = the engine/CLI default (prompts).
|
|
98
|
+
*/
|
|
99
|
+
permissionMode?: PermissionMode;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Structured, classified errors so the exit report can say *what* failed,
|
|
104
|
+
* *where* in the loop tree, and *why* — instead of dumping a stack.
|
|
105
|
+
*/
|
|
106
|
+
type LoopErrorCode = 'ENGINE' | 'TIMEOUT' | 'ABORTED' | 'VALIDATION' | 'CONFIG' | 'BUDGET' | 'RATE_LIMIT' | 'QUOTA' | 'BODY' | 'UNKNOWN';
|
|
107
|
+
type LoopPhase = 'start' | 'body' | 'until' | 'stopOn' | 'review' | 'engine';
|
|
108
|
+
interface LoopErrorInit {
|
|
109
|
+
code: LoopErrorCode;
|
|
110
|
+
message: string;
|
|
111
|
+
phase?: LoopPhase;
|
|
112
|
+
path?: readonly string[];
|
|
113
|
+
iteration?: number;
|
|
114
|
+
cause?: unknown;
|
|
115
|
+
retryable?: boolean;
|
|
116
|
+
/** Suggested wait before retry, in ms (e.g. a `retry-after` header). */
|
|
117
|
+
retryAfterMs?: number;
|
|
118
|
+
/** When the limit resets, as epoch ms. The wait policy prefers this. */
|
|
119
|
+
resetAt?: number;
|
|
120
|
+
}
|
|
121
|
+
declare class LoopError extends Error {
|
|
122
|
+
readonly code: LoopErrorCode;
|
|
123
|
+
readonly phase?: LoopPhase;
|
|
124
|
+
readonly path?: readonly string[];
|
|
125
|
+
readonly iteration?: number;
|
|
126
|
+
readonly retryable: boolean;
|
|
127
|
+
/** Suggested wait before retry, in ms (e.g. a `retry-after` header). */
|
|
128
|
+
readonly retryAfterMs?: number;
|
|
129
|
+
/** When the limit resets, as epoch ms. The wait policy prefers this. */
|
|
130
|
+
readonly resetAt?: number;
|
|
131
|
+
constructor(init: LoopErrorInit);
|
|
132
|
+
/** Wrap an arbitrary thrown value, preserving a `LoopError` as-is. */
|
|
133
|
+
static from(value: unknown, fallback: Omit<LoopErrorInit, 'message' | 'cause'>): LoopError;
|
|
134
|
+
toJSON(): {
|
|
135
|
+
name: string;
|
|
136
|
+
code: LoopErrorCode;
|
|
137
|
+
message: string;
|
|
138
|
+
phase: LoopPhase | undefined;
|
|
139
|
+
path: readonly string[] | undefined;
|
|
140
|
+
iteration: number | undefined;
|
|
141
|
+
retryable: boolean;
|
|
142
|
+
retryAfterMs: number | undefined;
|
|
143
|
+
resetAt: number | undefined;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A token-denominated budget for a whole run, threaded through the JobContext so
|
|
149
|
+
* every engine call site can refuse to spend past the cap. The honest cost guard
|
|
150
|
+
* for a loop that may fire a worker plus several judges per iteration: `max` and
|
|
151
|
+
* depth bound the *count* of calls, this bounds their *cost*.
|
|
152
|
+
*
|
|
153
|
+
* The runner feeds `add()` from each `engine:usage` event, so `spent()` is live.
|
|
154
|
+
* `assertBudget(ctx)` runs before an engine call; once the cap is reached it
|
|
155
|
+
* throws a non-retryable BUDGET error (hard mode, terminates the run) or logs
|
|
156
|
+
* and continues (soft mode, for exploratory runs).
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
interface BudgetConfig {
|
|
160
|
+
/** Cap on total tokens (input + output) for the whole run. */
|
|
161
|
+
limit: number;
|
|
162
|
+
/**
|
|
163
|
+
* Refuse a new engine call once `spent + headroom >= limit`, i.e. stop with
|
|
164
|
+
* room to spare rather than only after the cap is already blown. Default 0.
|
|
165
|
+
*/
|
|
166
|
+
headroom?: number;
|
|
167
|
+
/** Warn and continue instead of refusing when the cap is hit. Default false. */
|
|
168
|
+
soft?: boolean;
|
|
169
|
+
}
|
|
170
|
+
declare class Budget {
|
|
171
|
+
readonly limit: number;
|
|
172
|
+
readonly headroom: number;
|
|
173
|
+
readonly soft: boolean;
|
|
174
|
+
private tokens;
|
|
175
|
+
constructor(config: BudgetConfig);
|
|
176
|
+
/** Record consumed tokens. Non-finite or non-positive values are ignored. */
|
|
177
|
+
add(tokens: number): void;
|
|
178
|
+
spent(): number;
|
|
179
|
+
remaining(): number;
|
|
180
|
+
/** True once the next call would breach the cap (accounting for headroom). */
|
|
181
|
+
exceeded(): boolean;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* AgentDef — a reusable, job-specific agent definition. The persona and methodology
|
|
186
|
+
* (the prose: `system`, skill `instructions`) live in editable markdown files; the
|
|
187
|
+
* structure and types live here in TypeScript. The `.ts` is the strongly-typed wrapper
|
|
188
|
+
* around the `.md` — author the prompt as markdown, get type safety and validation in code.
|
|
189
|
+
*
|
|
190
|
+
* Grounded in the amps-os agent profile, minus the amps-specific machinery loops already
|
|
191
|
+
* provides: `dag` is the dispatcher, `conditions`/`quorum` are the gates, `Outcome` is the
|
|
192
|
+
* result channel. So AgentDef is just the contract — who the agent is, what it may touch,
|
|
193
|
+
* how it works — that `agentJob` resolves into an engine request.
|
|
194
|
+
*/
|
|
195
|
+
/** A skill is a METHODOLOGY (how to do the work — TDD, writing-plans), not a worker.
|
|
196
|
+
* An agent composes skills; a skill never dispatches an agent. */
|
|
197
|
+
interface Skill {
|
|
198
|
+
name: string;
|
|
199
|
+
/** The methodology instructions — prepended to the agent's system when it applies them. */
|
|
200
|
+
instructions: string;
|
|
201
|
+
}
|
|
202
|
+
interface AgentDef {
|
|
203
|
+
/** Identity (also the default job label). */
|
|
204
|
+
name: string;
|
|
205
|
+
/** What and why — for humans, docs, and (if loops scales) discovery. */
|
|
206
|
+
description?: string;
|
|
207
|
+
/** The system prompt: who the agent is and how it works. Use `fromFile('x.md')`. */
|
|
208
|
+
system: string;
|
|
209
|
+
/** Model id; omitted = inherit the run default. */
|
|
210
|
+
model?: string;
|
|
211
|
+
/** Allowed tool names — the permission boundary. */
|
|
212
|
+
tools?: string[];
|
|
213
|
+
/**
|
|
214
|
+
* Mark this agent a leaf: it may not spawn sub-agents / fan out (the engine disallows
|
|
215
|
+
* the sub-agent tool). Use it to control where a branch of the graph bottoms out — to
|
|
216
|
+
* stop a thorough agent from quietly expanding into a slow, expensive swarm.
|
|
217
|
+
*/
|
|
218
|
+
leaf?: boolean;
|
|
219
|
+
/** Structured job descriptions (not prose) — for discovery / docs. */
|
|
220
|
+
capabilities?: string[];
|
|
221
|
+
/** Methodologies the agent applies; their instructions are folded into the system. */
|
|
222
|
+
skills?: Skill[];
|
|
223
|
+
/** Named failure modes + their recovery — first-class contracts, not buried prose. */
|
|
224
|
+
failureModes?: {
|
|
225
|
+
mode: string;
|
|
226
|
+
recovery: string;
|
|
227
|
+
}[];
|
|
228
|
+
}
|
|
229
|
+
/** Read a markdown file as a string — for `system` or skill `instructions`. Pass an
|
|
230
|
+
* absolute path, or `new URL('./x.md', import.meta.url)` for a path relative to the file. */
|
|
231
|
+
declare function fromFile(path: string | URL): string;
|
|
232
|
+
/** Define a skill (a methodology). Identity + validation; strongly typed. */
|
|
233
|
+
declare function defineSkill(skill: Skill): Skill;
|
|
234
|
+
/** Define an agent. Identity + validation; strongly typed (the wrapper around the md). */
|
|
235
|
+
declare function defineAgent(def: AgentDef): AgentDef;
|
|
236
|
+
/**
|
|
237
|
+
* Resolve an agent's system prompt, folding in its skills' methodologies. This is what
|
|
238
|
+
* `agentJob` hands the engine as `system`.
|
|
239
|
+
*/
|
|
240
|
+
declare function resolveSystem(agent: AgentDef): string;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Job builders. A `Job` is the unit of work; these are the common shapes.
|
|
244
|
+
* The agent launch (`agentJob`) is deliberately provider-agnostic: it only
|
|
245
|
+
* ever calls `Engine.run`, so it knows nothing about Claude, the CLI, an SDK,
|
|
246
|
+
* an HTTP API, or any framework — swap the engine and the same job runs.
|
|
247
|
+
*/
|
|
248
|
+
|
|
249
|
+
interface AgentJobConfig {
|
|
250
|
+
/** Job label (for events). Defaults to the agent's name, then `'agent'`. */
|
|
251
|
+
label?: string;
|
|
252
|
+
/**
|
|
253
|
+
* A reusable agent definition — supplies `system` (persona + skills), `model`, and
|
|
254
|
+
* `tools` (the job's `system`/`model`/`allowedTools` override it when also set). The
|
|
255
|
+
* persona lives in markdown via `fromFile`; this is the typed wrapper around it.
|
|
256
|
+
*/
|
|
257
|
+
agent?: AgentDef;
|
|
258
|
+
/** The prompt, or a function of the context (e.g. include the iteration). */
|
|
259
|
+
prompt: string | ((ctx: JobContext) => string | Promise<string>);
|
|
260
|
+
system?: string | ((ctx: JobContext) => string);
|
|
261
|
+
/** Engine override: a registered name, your own `Engine`, or the default. */
|
|
262
|
+
engine?: EngineRef;
|
|
263
|
+
/** Bare model id — passed straight through to the engine. */
|
|
264
|
+
model?: string;
|
|
265
|
+
maxTokens?: number;
|
|
266
|
+
allowedTools?: string[];
|
|
267
|
+
/**
|
|
268
|
+
* Mark this turn a leaf: forbid spawning sub-agents (the engine disallows the sub-agent
|
|
269
|
+
* tool), so a branch bottoms out here. Falls back to the agent def's `leaf`.
|
|
270
|
+
*/
|
|
271
|
+
leaf?: boolean;
|
|
272
|
+
/** Working dir for the turn. Default: the workspace dir (the worktree). */
|
|
273
|
+
cwd?: string;
|
|
274
|
+
timeoutMs?: number;
|
|
275
|
+
/**
|
|
276
|
+
* Ground the turn in memory before it works: prepend the branch-local commit log
|
|
277
|
+
* (recent committed milestones), the live working memory (`ledger.md`) and handoff
|
|
278
|
+
* (`prompt.md`) from this run, and tell the agent where to record its own
|
|
279
|
+
* reasoning. With grounding on, the harness also auto-captures the turn into
|
|
280
|
+
* `ledger.md` afterwards. `true` uses defaults; an object tunes the reach. This is
|
|
281
|
+
* how a fresh context stops repeating what earlier iterations already tried.
|
|
282
|
+
*/
|
|
283
|
+
ground?: boolean | GroundConfig;
|
|
284
|
+
/**
|
|
285
|
+
* Map the agent's raw text into an `Outcome`. Default: `pass`, with the text
|
|
286
|
+
* as the summary. Return `fail` to keep an enclosing loop going.
|
|
287
|
+
*/
|
|
288
|
+
outcome?: (text: string, ctx: JobContext) => Outcome | Promise<Outcome>;
|
|
289
|
+
}
|
|
290
|
+
interface GroundConfig {
|
|
291
|
+
/** Max committed milestones to include (newest first). Default 10. */
|
|
292
|
+
max?: number;
|
|
293
|
+
/** Truncate each commit body to this many chars. Default 1200. */
|
|
294
|
+
bodyChars?: number;
|
|
295
|
+
/** Include the live scratch files (this run's working memory + handoff). Default true. */
|
|
296
|
+
includeScratch?: boolean;
|
|
297
|
+
/** Tell the agent to leave memory for the next agent. Default true. */
|
|
298
|
+
recordInstruction?: boolean;
|
|
299
|
+
/**
|
|
300
|
+
* Retrieve relevant commits with a cheap model instead of taking recent-N.
|
|
301
|
+
* Far less noisy when the branch log carries unrelated work (a shared repo).
|
|
302
|
+
* `true` uses defaults; an object tunes the candidate window / selection model.
|
|
303
|
+
*/
|
|
304
|
+
retrieve?: boolean | {
|
|
305
|
+
candidates?: number;
|
|
306
|
+
model?: string;
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/** Run one fresh agent turn through whichever engine is selected. */
|
|
310
|
+
declare function agentJob(config: AgentJobConfig): Job;
|
|
311
|
+
interface CommitJobConfig {
|
|
312
|
+
label?: string;
|
|
313
|
+
/** Conventional-commit subject, or a function of context + last outcome. */
|
|
314
|
+
subject: string | ((ctx: JobContext, last: Outcome | undefined) => string | Promise<string>);
|
|
315
|
+
/**
|
|
316
|
+
* The "way" — the structured commit body. Precedence when composing it:
|
|
317
|
+
* 1. this `body` (an explicit override — a string or function), else
|
|
318
|
+
* 2. the scratch files: the handoff (`prompt.md`) plus a compacted working log
|
|
319
|
+
* (`ledger.md`) — the trusted source, capturing the why as it happens across
|
|
320
|
+
* a long unit of work and across fanned-out sub-agents, else
|
|
321
|
+
* 3. a default composed from the last outcome (the floor).
|
|
322
|
+
* Set `body` only to override the scratch files.
|
|
323
|
+
*/
|
|
324
|
+
body?: string | ((ctx: JobContext, last: Outcome | undefined) => string | Promise<string>);
|
|
325
|
+
/** Model for the (cheap) ledger-compaction call. A small one is plenty. */
|
|
326
|
+
compactModel?: string;
|
|
327
|
+
/** Stage every change before committing (default true). */
|
|
328
|
+
stageAll?: boolean;
|
|
329
|
+
/** Commit even with nothing staged (default false → a no-op `pass`). */
|
|
330
|
+
allowEmpty?: boolean;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Commit the workspace — write the "way" (a structured body) welded to the
|
|
334
|
+
* "what" (the staged diff) onto the work branch. This is the loop's memory: the
|
|
335
|
+
* next fresh context reads these commits back. The body is composed from the
|
|
336
|
+
* scratch files (the handoff plus a compacted working log agents accrued as they
|
|
337
|
+
* worked), falling back to the outcome floor — so the rich why survives context
|
|
338
|
+
* decay and fan-out. Both scratch files are cleared once the commit lands.
|
|
339
|
+
* Engine-agnostic; it only touches `ctx.workspace.dir` and git. A non-repo
|
|
340
|
+
* workspace fails loudly (a non-retryable CONFIG error) rather than silently
|
|
341
|
+
* dropping the work's record.
|
|
342
|
+
*/
|
|
343
|
+
declare function commitJob(config: CommitJobConfig): Job;
|
|
344
|
+
/**
|
|
345
|
+
* Build an `Outcome` that sends work back to an earlier dag node — real-team
|
|
346
|
+
* feedback ("marketing found the contract drifted; re-run engineering"). Return
|
|
347
|
+
* it from any job or `agentJob({ outcome })` mapper. The enclosing `dag` re-runs
|
|
348
|
+
* `to` and its dependents with `reason` threaded in as `lastReview`, bounded by
|
|
349
|
+
* `DagConfig.maxKickbacks`. Defaults to a `fail` status, so an unresolved
|
|
350
|
+
* kickback (budget spent) leaves the dag failing honestly; override via `over`
|
|
351
|
+
* (e.g. `{ status: 'pass' }`) when the kicking node's own work is fine and it is
|
|
352
|
+
* only requesting an upstream revision.
|
|
353
|
+
*/
|
|
354
|
+
declare function kickback(to: string, reason: string, over?: Partial<Outcome>): Outcome;
|
|
355
|
+
/** A deterministic step from a plain function — for glue, checks, side effects. */
|
|
356
|
+
declare function fnJob(label: string, fn: (ctx: JobContext) => Outcome | Promise<Outcome>): Job;
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The Environment provider — the third axis, after Engine (where the agent
|
|
360
|
+
* thinks) and Workspace (where the code lives). Environment is where the code
|
|
361
|
+
* RUNS: local services, or a per-branch cloud preview. It is what lets the gate
|
|
362
|
+
* be fully honest — "done" can mean "the e2e suite passes against the running
|
|
363
|
+
* preview", not just "unit tests pass against static files on disk".
|
|
364
|
+
*
|
|
365
|
+
* Like `Engine`, this is only an interface. loops owns the seam and the
|
|
366
|
+
* lifecycle binding; the actual adapter (sst, Vercel, Docker, …) is
|
|
367
|
+
* provider-specific and lives in the CONSUMER's loop definition, next to the
|
|
368
|
+
* deploy config it wraps. loops never takes a dependency on a deploy tool. Bring
|
|
369
|
+
* your own in a few lines: implement `up`, return a handle.
|
|
370
|
+
*
|
|
371
|
+
* const sstEnv: Environment = {
|
|
372
|
+
* name: 'sst',
|
|
373
|
+
* async up(ws) {
|
|
374
|
+
* const stage = slug(ws.branch); // per-branch stage
|
|
375
|
+
* const out = await sh('sst', ['deploy', '--stage', stage], ws.dir);
|
|
376
|
+
* return {
|
|
377
|
+
* url: out.url,
|
|
378
|
+
* env: { BASE_URL: out.url },
|
|
379
|
+
* down: () => sh('sst', ['remove', '--stage', stage], ws.dir),
|
|
380
|
+
* };
|
|
381
|
+
* },
|
|
382
|
+
* };
|
|
383
|
+
*/
|
|
384
|
+
|
|
385
|
+
/** A running environment for one workspace. Returned by `Environment.up`. */
|
|
386
|
+
interface EnvHandle {
|
|
387
|
+
/** Addressable base URL (a preview deployment, or a local server), if any. */
|
|
388
|
+
readonly url?: string;
|
|
389
|
+
/**
|
|
390
|
+
* Variables injected into gate commands (and, later, agent turns) — e.g.
|
|
391
|
+
* `BASE_URL`, `DATABASE_URL`. This is how `commandSucceeds('playwright', …)`
|
|
392
|
+
* reaches the running preview.
|
|
393
|
+
*/
|
|
394
|
+
readonly env: Record<string, string>;
|
|
395
|
+
/**
|
|
396
|
+
* Redeploy when the branch advances (a cloud preview that tracks commits).
|
|
397
|
+
* Optional: a local-services env has nothing to sync.
|
|
398
|
+
*/
|
|
399
|
+
sync?(commit: string, signal: AbortSignal): Promise<void>;
|
|
400
|
+
/** Tear the environment down. */
|
|
401
|
+
down(signal: AbortSignal): Promise<void>;
|
|
402
|
+
}
|
|
403
|
+
/** Brings a workspace's code up so the gate can test the running thing. */
|
|
404
|
+
interface Environment {
|
|
405
|
+
readonly name: string;
|
|
406
|
+
up(workspace: Workspace, signal: AbortSignal): Promise<EnvHandle>;
|
|
407
|
+
}
|
|
408
|
+
/** Duck-type guard: a ready-made `Environment` rather than something else. */
|
|
409
|
+
declare function isEnvironment(value: unknown): value is Environment;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The Forge provider — the host where a branch becomes a pull request. It sits
|
|
413
|
+
* alongside Engine (where the agent thinks) and Environment (where the code runs)
|
|
414
|
+
* as a thin, swappable seam: loops owns the interface and the jobs that drive it
|
|
415
|
+
* (`pullRequestJob`, `mergeJob` in `pr.ts`); the default adapter shells out to the
|
|
416
|
+
* GitHub CLI (`gh`), the same subprocess instinct as the `git`/claude-cli engines.
|
|
417
|
+
*
|
|
418
|
+
* Why a seam at all: the squash-merge boundary is where loops' commit-log memory
|
|
419
|
+
* would otherwise be lost. A PR carries a body, and a squash merge can be made to
|
|
420
|
+
* use that body as the merged commit message — so if loops keeps the PR body a
|
|
421
|
+
* faithful synthesis of the branch's commit "ways", the Ledger survives the squash.
|
|
422
|
+
* The Forge is how loops reaches the PR to write that body and (optionally) drive
|
|
423
|
+
* the merge. A `MockForge` keeps the jobs offline-testable, the loops convention.
|
|
424
|
+
*/
|
|
425
|
+
/** Identifies a pull request on the host. */
|
|
426
|
+
interface PrRef {
|
|
427
|
+
number: number;
|
|
428
|
+
url: string;
|
|
429
|
+
/** The head branch the PR is for. */
|
|
430
|
+
branch?: string;
|
|
431
|
+
}
|
|
432
|
+
/** Everything needed to open a PR. */
|
|
433
|
+
interface PrInput {
|
|
434
|
+
title: string;
|
|
435
|
+
body: string;
|
|
436
|
+
/** The branch to merge into (e.g. `main`). */
|
|
437
|
+
base: string;
|
|
438
|
+
/** The branch carrying the work (the PR head). */
|
|
439
|
+
branch: string;
|
|
440
|
+
draft?: boolean;
|
|
441
|
+
}
|
|
442
|
+
/** A partial update to an existing PR (the body is the synthesis we keep current). */
|
|
443
|
+
interface PrPatch {
|
|
444
|
+
title?: string;
|
|
445
|
+
body?: string;
|
|
446
|
+
}
|
|
447
|
+
/** Where the host command runs (the repo working dir) + the run's abort signal. */
|
|
448
|
+
interface ForgeOpts {
|
|
449
|
+
cwd: string;
|
|
450
|
+
signal?: AbortSignal;
|
|
451
|
+
}
|
|
452
|
+
interface MergeOptions extends ForgeOpts {
|
|
453
|
+
/** Squash merge (default). The whole point — collapse the branch to one commit. */
|
|
454
|
+
squash?: boolean;
|
|
455
|
+
/** The squash commit subject. */
|
|
456
|
+
subject?: string;
|
|
457
|
+
/** The squash commit body — the synthesis, written directly so it cannot be lost. */
|
|
458
|
+
body?: string;
|
|
459
|
+
/** Enqueue GitHub auto-merge: the merge happens once required checks pass. */
|
|
460
|
+
auto?: boolean;
|
|
461
|
+
/** Delete the head branch after merge. */
|
|
462
|
+
deleteBranch?: boolean;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* The host seam. Five operations, each taking an explicit working dir — no hidden
|
|
466
|
+
* global state, mirroring `git.ts`. `viewPr` answers an expected "no" with
|
|
467
|
+
* `undefined` (no PR yet); the mutating ops throw a clear `CONFIG` error when the
|
|
468
|
+
* CLI is missing/unauthed, never a cryptic crash.
|
|
469
|
+
*/
|
|
470
|
+
interface Forge {
|
|
471
|
+
readonly name: string;
|
|
472
|
+
/** The open PR whose head is `branch`, or undefined when there is none. */
|
|
473
|
+
viewPr(branch: string, opts: ForgeOpts): Promise<PrRef | undefined>;
|
|
474
|
+
createPr(input: PrInput, opts: ForgeOpts): Promise<PrRef>;
|
|
475
|
+
editPr(pr: PrRef, patch: PrPatch, opts: ForgeOpts): Promise<void>;
|
|
476
|
+
mergePr(pr: PrRef, opts: MergeOptions): Promise<void>;
|
|
477
|
+
/** True when the PR's required checks are all green (for a synchronous gate). */
|
|
478
|
+
checksPass(pr: PrRef, opts: ForgeOpts): Promise<boolean>;
|
|
479
|
+
}
|
|
480
|
+
/** Duck-type guard: a ready-made `Forge` rather than something else. */
|
|
481
|
+
declare function isForge(value: unknown): value is Forge;
|
|
482
|
+
declare function buildViewArgs(branch: string): string[];
|
|
483
|
+
declare function buildCreateArgs(input: PrInput): string[];
|
|
484
|
+
declare function buildEditArgs(pr: PrRef, patch: PrPatch): string[];
|
|
485
|
+
declare function buildMergeArgs(pr: PrRef, opts: MergeOptions): string[];
|
|
486
|
+
declare function buildChecksArgs(pr: PrRef): string[];
|
|
487
|
+
/** The GitHub CLI adapter. Everything `pr.ts` needs, over `gh`. */
|
|
488
|
+
declare class GhForge implements Forge {
|
|
489
|
+
private readonly bin;
|
|
490
|
+
readonly name = "gh";
|
|
491
|
+
constructor(bin?: string);
|
|
492
|
+
viewPr(branch: string, opts: ForgeOpts): Promise<PrRef | undefined>;
|
|
493
|
+
createPr(input: PrInput, opts: ForgeOpts): Promise<PrRef>;
|
|
494
|
+
editPr(pr: PrRef, patch: PrPatch, opts: ForgeOpts): Promise<void>;
|
|
495
|
+
mergePr(pr: PrRef, opts: MergeOptions): Promise<void>;
|
|
496
|
+
checksPass(pr: PrRef, opts: ForgeOpts): Promise<boolean>;
|
|
497
|
+
}
|
|
498
|
+
interface MockForgeOptions {
|
|
499
|
+
/** Branch → a PR that already exists (so the job takes the update path). */
|
|
500
|
+
existing?: Record<string, PrRef>;
|
|
501
|
+
/** What `checksPass` returns. Default true. */
|
|
502
|
+
checks?: boolean;
|
|
503
|
+
}
|
|
504
|
+
/** Records every call and keeps a tiny in-memory PR store. No network. */
|
|
505
|
+
declare class MockForge implements Forge {
|
|
506
|
+
private readonly opts;
|
|
507
|
+
readonly name = "mock-forge";
|
|
508
|
+
readonly calls: {
|
|
509
|
+
method: string;
|
|
510
|
+
args: Record<string, unknown>;
|
|
511
|
+
}[];
|
|
512
|
+
private readonly prs;
|
|
513
|
+
private seq;
|
|
514
|
+
constructor(opts?: MockForgeOptions);
|
|
515
|
+
viewPr(branch: string): Promise<PrRef | undefined>;
|
|
516
|
+
createPr(input: PrInput): Promise<PrRef>;
|
|
517
|
+
editPr(pr: PrRef, patch: PrPatch): Promise<void>;
|
|
518
|
+
mergePr(pr: PrRef, opts: MergeOptions): Promise<void>;
|
|
519
|
+
checksPass(): Promise<boolean>;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* The core contract. Borrowing the Jenkins instinct — "everything is a job" —
|
|
524
|
+
* there is one universal runnable unit and two supporting types:
|
|
525
|
+
*
|
|
526
|
+
* - a `Job` — a unit of work that runs once and returns an `Outcome`.
|
|
527
|
+
* Any size: a single agent turn, or a whole nested loop.
|
|
528
|
+
* - a `Condition` — a question answered against the current context (a `when`).
|
|
529
|
+
* - a `Loop` — produced by `loop()`, and is *itself a `Job`*.
|
|
530
|
+
*
|
|
531
|
+
* Because a `Loop` is a `Job`, a loop's `body`/`review`/any stage can be
|
|
532
|
+
* another `loop(...)`. Nesting is the absence of a special case, not a feature.
|
|
533
|
+
*
|
|
534
|
+
* The Jenkins mapping, for the parts we borrow: Job≈job/pipeline, Engine≈agent/
|
|
535
|
+
* node (where it runs), `start`≈trigger, `Condition`≈`when`, `review`+`onComplete`
|
|
536
|
+
* ≈`post`, `retry`≈`retry`/`catchError`. We deliberately do NOT import the
|
|
537
|
+
* stage/DAG machinery — the primitive here is the loop, not a pipeline.
|
|
538
|
+
*/
|
|
539
|
+
|
|
540
|
+
/** Terminal disposition of a `Job`. */
|
|
541
|
+
type OutcomeStatus = 'pass' | 'fail' | 'aborted' | 'exhausted' | 'paused';
|
|
542
|
+
/**
|
|
543
|
+
* How the run reacts to a provider rate limit, account/usage allowance, or its
|
|
544
|
+
* own token budget. `auto` (the default) waits when the reset is known and
|
|
545
|
+
* within `maxWaitMs`, else checkpoints and exits with a resume command.
|
|
546
|
+
*/
|
|
547
|
+
type LimitPolicy = 'auto' | 'wait' | 'exit-resume' | 'fail';
|
|
548
|
+
interface Outcome {
|
|
549
|
+
status: OutcomeStatus;
|
|
550
|
+
/** 0..1 confidence, when the outcome was decided by an agent validator. */
|
|
551
|
+
confidence?: number;
|
|
552
|
+
/** One-line human summary, surfaced in the TUI and exit report. */
|
|
553
|
+
summary?: string;
|
|
554
|
+
/** Arbitrary payload threaded to the next step / surfaced to the caller. */
|
|
555
|
+
data?: unknown;
|
|
556
|
+
/** Present when `status` is driven by a failure. */
|
|
557
|
+
error?: LoopError;
|
|
558
|
+
/**
|
|
559
|
+
* A request to send work back to an earlier dag node (real-team feedback: a
|
|
560
|
+
* later stage that found a problem upstream). The enclosing `dag` re-runs `to`
|
|
561
|
+
* and its transitive dependents with `reason` threaded in as `lastReview`,
|
|
562
|
+
* bounded by `DagConfig.maxKickbacks` (default 0 — ignored). A feedback cycle
|
|
563
|
+
* is a loop boundary, not a backward edge: the graph stays acyclic and the
|
|
564
|
+
* re-run budget guarantees it terminates. Use the `kickback(to, reason)`
|
|
565
|
+
* helper to produce one.
|
|
566
|
+
*/
|
|
567
|
+
kickback?: {
|
|
568
|
+
to: string;
|
|
569
|
+
reason: string;
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
573
|
+
/**
|
|
574
|
+
* Where a job's code lives: a working directory and (when it is a git repo) the
|
|
575
|
+
* branch checked out there. This is the substrate the commit ledger is written
|
|
576
|
+
* to and read back from. A sequential loop's iterations share one `Workspace`
|
|
577
|
+
* (the ledger accumulates on one branch); concurrency is where workspaces fork
|
|
578
|
+
* into isolated worktrees. Default: the process working directory.
|
|
579
|
+
*/
|
|
580
|
+
interface Workspace {
|
|
581
|
+
/** Absolute path to the working tree this job operates in. */
|
|
582
|
+
readonly dir: string;
|
|
583
|
+
/** The branch checked out in `dir`, when known (undefined on detached HEAD). */
|
|
584
|
+
readonly branch?: string;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Threaded into every `Job`. Carries the engine, the abort signal, the event
|
|
588
|
+
* sink, a mutable scratchpad shared across the run, the workspace the work
|
|
589
|
+
* happens in, and the position in the loop tree (used by the TUI and stats).
|
|
590
|
+
*/
|
|
591
|
+
interface JobContext {
|
|
592
|
+
/** Default engine for this run; overridable per-step via `resolveEngine`. */
|
|
593
|
+
readonly engine: Engine;
|
|
594
|
+
/**
|
|
595
|
+
* Resolve an engine for a step. Accepts a registered name, a ready-made
|
|
596
|
+
* `Engine` (bring-your-own provider/framework), or nothing (the run default).
|
|
597
|
+
*/
|
|
598
|
+
resolveEngine(ref?: EngineRef): Engine;
|
|
599
|
+
readonly signal: AbortSignal;
|
|
600
|
+
emit(event: LoopEvent): void;
|
|
601
|
+
/** Shared mutable state for the whole run (e.g. accumulating notes). */
|
|
602
|
+
readonly state: Record<string, unknown>;
|
|
603
|
+
/** Where this job's code lives — the working dir and branch (the substrate). */
|
|
604
|
+
readonly workspace: Workspace;
|
|
605
|
+
/** The running environment for this workspace, when one is up (gate target). */
|
|
606
|
+
readonly environment?: EnvHandle;
|
|
607
|
+
/** The PR host, when one is configured — where `pullRequestJob`/`mergeJob` run. */
|
|
608
|
+
readonly forge?: Forge;
|
|
609
|
+
/** 1-based iteration index within the enclosing loop; 0 outside a loop. */
|
|
610
|
+
readonly iteration: number;
|
|
611
|
+
/** Nesting depth (root steps are 0). */
|
|
612
|
+
readonly depth: number;
|
|
613
|
+
/** Loop/step names from the root down to here. */
|
|
614
|
+
readonly path: readonly string[];
|
|
615
|
+
/** The previous body outcome in the enclosing loop (used by `review`/gates). */
|
|
616
|
+
readonly lastOutcome?: Outcome;
|
|
617
|
+
/** The most recent failed-review outcome, so a restart can act on it. */
|
|
618
|
+
readonly lastReview?: Outcome;
|
|
619
|
+
/** The run's token budget, when one is set; engine call sites guard on it. */
|
|
620
|
+
readonly budget?: Budget;
|
|
621
|
+
/** How a loop reacts to a rate/quota/budget limit. Default `auto`. */
|
|
622
|
+
readonly onLimit: LimitPolicy;
|
|
623
|
+
/** Cap on an interruptible limit-wait under `auto`/`wait`. */
|
|
624
|
+
readonly maxWaitMs: number;
|
|
625
|
+
/** Ready-to-paste command to resume a paused run, when reconstructable. */
|
|
626
|
+
readonly resumeCommand?: string;
|
|
627
|
+
log(message: string, level?: LogLevel): void;
|
|
628
|
+
}
|
|
629
|
+
type Job = (ctx: JobContext) => Promise<Outcome>;
|
|
630
|
+
/**
|
|
631
|
+
* The introspectable shape of a `Job`, attached by the builders (`loop`, `dag`,
|
|
632
|
+
* `agentJob`, ...) and read back by `loops validate` / `loops describe` and any
|
|
633
|
+
* agent that wants to inspect a loop without running it. Held in a side table
|
|
634
|
+
* (see `core/describe.ts`), so the `Job` type stays a plain function. `kind`
|
|
635
|
+
* names the builder; the rest is builder-specific (a loop carries its gate and
|
|
636
|
+
* body, a dag carries its nodes).
|
|
637
|
+
*/
|
|
638
|
+
interface JobMeta {
|
|
639
|
+
kind: 'loop' | 'dag' | 'agent' | 'fn' | (string & {});
|
|
640
|
+
name?: string;
|
|
641
|
+
[key: string]: unknown;
|
|
642
|
+
}
|
|
643
|
+
interface ConditionResult {
|
|
644
|
+
met: boolean;
|
|
645
|
+
/** 0..1 when an agent decided this; undefined for deterministic checks. */
|
|
646
|
+
confidence?: number;
|
|
647
|
+
reason: string;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* The single condition primitive. A question answered against the context and
|
|
651
|
+
* the most recent body outcome. Both deterministic checks and agent validators
|
|
652
|
+
* are this same type — `agentCheck(...)` simply returns one.
|
|
653
|
+
*/
|
|
654
|
+
type Condition = (ctx: JobContext, last: Outcome | undefined) => Promise<ConditionResult>;
|
|
655
|
+
/** A bare deterministic predicate — accepted anywhere a `Condition` is. */
|
|
656
|
+
type RawPredicate = (ctx: JobContext, last: Outcome | undefined) => boolean | Promise<boolean>;
|
|
657
|
+
/**
|
|
658
|
+
* What a gate (`start`/`until`/`stopOn`) accepts: one item or many, freely
|
|
659
|
+
* mixing deterministic predicates and agent conditions. Arrays are reduced to
|
|
660
|
+
* the single `Condition` primitive by `toCondition` (default: all must hold;
|
|
661
|
+
* wrap in `any(...)` for or-semantics).
|
|
662
|
+
*/
|
|
663
|
+
type ConditionInput = Condition | RawPredicate | ConditionInput[];
|
|
664
|
+
interface RetryPolicy {
|
|
665
|
+
/** On a thrown error in the body: keep looping, or end the loop as failed. */
|
|
666
|
+
onError: 'continue' | 'fail';
|
|
667
|
+
/** Cap on consecutive errored iterations before forcing 'fail'. */
|
|
668
|
+
maxConsecutive?: number;
|
|
669
|
+
backoffMs?: number;
|
|
670
|
+
}
|
|
671
|
+
interface LoopConfig {
|
|
672
|
+
name: string;
|
|
673
|
+
/** The work done each iteration. Pass another `loop(...)` to nest. */
|
|
674
|
+
body: Job;
|
|
675
|
+
/** Gate before iterating; one or many checks. Unmet => loop is `aborted`. */
|
|
676
|
+
start?: ConditionInput;
|
|
677
|
+
/** After each body run; one or many checks. Met => stop (then `review`). */
|
|
678
|
+
until?: ConditionInput;
|
|
679
|
+
/** Hard early-exit per iteration; one or many checks. Met => `aborted`. */
|
|
680
|
+
stopOn?: ConditionInput;
|
|
681
|
+
/** Iteration cap. Reached without passing => `exhausted`. */
|
|
682
|
+
max?: number;
|
|
683
|
+
/**
|
|
684
|
+
* Runs when `until` is met. If it returns `pass`, the loop completes.
|
|
685
|
+
* Any other status re-enters the loop — this is the "review fails, run the
|
|
686
|
+
* main loop again" behaviour, and `review` may itself be a `loop(...)`. The
|
|
687
|
+
* failed review outcome is exposed to the next iteration as `ctx.lastReview`.
|
|
688
|
+
*/
|
|
689
|
+
review?: Job;
|
|
690
|
+
/**
|
|
691
|
+
* Cap on consecutive failed reviews before giving up with `exhausted`.
|
|
692
|
+
* Bounds the review-restart cycle independently of `max`; strongly advised
|
|
693
|
+
* when `review` is set with no `max` (otherwise a worker/reviewer standoff
|
|
694
|
+
* never terminates). Default: unbounded (relies on `max`).
|
|
695
|
+
*/
|
|
696
|
+
maxReviewRestarts?: number;
|
|
697
|
+
/**
|
|
698
|
+
* Record a checkpoint commit when the loop converges — the milestone. A commit
|
|
699
|
+
* is a milestone, not an iteration: iterations accumulate the why in the draft,
|
|
700
|
+
* and `commitJob` composes one structured commit from it on convergence. `true`
|
|
701
|
+
* derives the subject from the converged outcome; pass a `CommitJobConfig` to
|
|
702
|
+
* set the subject/body. Off by default. Finer granularity comes from finer
|
|
703
|
+
* structure (more loops/nodes), not per-iteration commits.
|
|
704
|
+
*/
|
|
705
|
+
commit?: boolean | CommitJobConfig;
|
|
706
|
+
/** Delay between iterations (polling intervals). Interruptible by abort. */
|
|
707
|
+
delayMs?: number;
|
|
708
|
+
retry?: RetryPolicy;
|
|
709
|
+
/** Side-effect hook after each iteration (logging, custom stats). */
|
|
710
|
+
onIteration?: (outcome: Outcome, ctx: JobContext) => void | Promise<void>;
|
|
711
|
+
/**
|
|
712
|
+
* Post-action run exactly once when the loop ends, whatever the status
|
|
713
|
+
* (Jenkins `post { always }`). For cleanup, notifications, final logging.
|
|
714
|
+
*/
|
|
715
|
+
onComplete?: (outcome: Outcome, ctx: JobContext) => void | Promise<void>;
|
|
716
|
+
}
|
|
717
|
+
interface DagNode {
|
|
718
|
+
job: Job;
|
|
719
|
+
/** Names of nodes that must finish (passing) before this one runs. */
|
|
720
|
+
needs?: string[];
|
|
721
|
+
/** Gate (one or many) — when unmet the node is skipped, not failed. */
|
|
722
|
+
when?: ConditionInput;
|
|
723
|
+
/** A failure here does not fail the DAG, and does not block dependents. */
|
|
724
|
+
optional?: boolean;
|
|
725
|
+
/**
|
|
726
|
+
* Run this node in its own git worktree on a fork branch (branches-as-teams).
|
|
727
|
+
* Concurrent writers then never collide on files or the index, and the node's
|
|
728
|
+
* committed work lands back into the parent branch on pass. Defaults to the
|
|
729
|
+
* DAG's `isolation`. Opt-in: forking a worktree has a real setup cost, and a
|
|
730
|
+
* read-only node never needs it.
|
|
731
|
+
*/
|
|
732
|
+
isolate?: boolean;
|
|
733
|
+
/**
|
|
734
|
+
* Restrict which upstream nodes this node may kick work back to. When set, a
|
|
735
|
+
* `kickback` whose `to` is not in this list is rejected (logged, not run); when
|
|
736
|
+
* unset, any ancestor is a valid target. A kickback to a non-ancestor is always
|
|
737
|
+
* rejected. Only consulted when the dag's `maxKickbacks` is set.
|
|
738
|
+
*/
|
|
739
|
+
acceptsKickbackTo?: string[];
|
|
740
|
+
}
|
|
741
|
+
interface DagConfig {
|
|
742
|
+
name: string;
|
|
743
|
+
/** Node name → a `DagNode`, or a bare `Job` (shorthand for no deps/gates). */
|
|
744
|
+
nodes: Record<string, DagNode | Job>;
|
|
745
|
+
/** Max nodes running at once. Default: unbounded. */
|
|
746
|
+
concurrency?: number;
|
|
747
|
+
/** When a required node fails, abort the rest. Default: true. */
|
|
748
|
+
stopOnError?: boolean;
|
|
749
|
+
/**
|
|
750
|
+
* Default isolation for nodes that do not set `isolate`. `'worktree'` runs each
|
|
751
|
+
* such node in its own worktree + fork branch, landed back on pass. Off by
|
|
752
|
+
* default — the shared workspace.
|
|
753
|
+
*/
|
|
754
|
+
isolation?: 'worktree';
|
|
755
|
+
/**
|
|
756
|
+
* Give each ISOLATED node its own environment, brought up when its worktree
|
|
757
|
+
* forks and torn down when it joins — so every branch-team gets its own stage,
|
|
758
|
+
* named by the provider from the workspace branch. Requires isolation; a
|
|
759
|
+
* non-isolated node shares the workspace and gets no per-team env.
|
|
760
|
+
*/
|
|
761
|
+
environment?: Environment;
|
|
762
|
+
/**
|
|
763
|
+
* What to do when an isolated node's land-back conflicts. `'fail'` (default)
|
|
764
|
+
* fails the node honestly. `'synthesize'` runs `mergeSynthesis` — an agent
|
|
765
|
+
* resolves the conflict and writes a synthesised merge body.
|
|
766
|
+
*/
|
|
767
|
+
onConflict?: 'fail' | 'synthesize';
|
|
768
|
+
/**
|
|
769
|
+
* Total re-run budget for cross-stage feedback. When a node's outcome carries
|
|
770
|
+
* a `kickback`, the dag re-runs the target node and its transitive dependents,
|
|
771
|
+
* threading the reason in as `lastReview`. Each such re-run spends one unit of
|
|
772
|
+
* this budget; once it is exhausted, a further kickback is rejected and the dag
|
|
773
|
+
* terminates. Default 0 — kickbacks are ignored and behaviour is unchanged.
|
|
774
|
+
* This bound is what makes a feedback cycle provably terminate.
|
|
775
|
+
*/
|
|
776
|
+
maxKickbacks?: number;
|
|
777
|
+
}
|
|
778
|
+
/** Per-node disposition within a DAG run. */
|
|
779
|
+
type NodePhase = 'start' | 'skip' | 'done';
|
|
780
|
+
type ConditionKind = 'start' | 'until' | 'stopOn';
|
|
781
|
+
type LoopEvent = {
|
|
782
|
+
kind: 'loop:start';
|
|
783
|
+
ts: number;
|
|
784
|
+
path: string[];
|
|
785
|
+
depth: number;
|
|
786
|
+
max?: number;
|
|
787
|
+
} | {
|
|
788
|
+
kind: 'loop:iteration';
|
|
789
|
+
ts: number;
|
|
790
|
+
path: string[];
|
|
791
|
+
iteration: number;
|
|
792
|
+
} | {
|
|
793
|
+
kind: 'loop:condition';
|
|
794
|
+
ts: number;
|
|
795
|
+
path: string[];
|
|
796
|
+
which: ConditionKind;
|
|
797
|
+
result: ConditionResult;
|
|
798
|
+
} | {
|
|
799
|
+
kind: 'loop:review';
|
|
800
|
+
ts: number;
|
|
801
|
+
path: string[];
|
|
802
|
+
outcome: Outcome;
|
|
803
|
+
} | {
|
|
804
|
+
kind: 'loop:end';
|
|
805
|
+
ts: number;
|
|
806
|
+
path: string[];
|
|
807
|
+
outcome: Outcome;
|
|
808
|
+
iterations: number;
|
|
809
|
+
} | {
|
|
810
|
+
kind: 'limit:wait';
|
|
811
|
+
ts: number;
|
|
812
|
+
path: string[];
|
|
813
|
+
code: string;
|
|
814
|
+
waitMs: number;
|
|
815
|
+
/** Wall-clock epoch ms the wait ends at (ts + waitMs). */
|
|
816
|
+
resumeAt: number;
|
|
817
|
+
} | {
|
|
818
|
+
kind: 'limit:pause';
|
|
819
|
+
ts: number;
|
|
820
|
+
path: string[];
|
|
821
|
+
code: string;
|
|
822
|
+
reason: string;
|
|
823
|
+
resumeCommand?: string;
|
|
824
|
+
} | {
|
|
825
|
+
kind: 'dag:start';
|
|
826
|
+
ts: number;
|
|
827
|
+
path: string[];
|
|
828
|
+
depth: number;
|
|
829
|
+
nodes: string[];
|
|
830
|
+
} | {
|
|
831
|
+
kind: 'dag:node';
|
|
832
|
+
ts: number;
|
|
833
|
+
path: string[];
|
|
834
|
+
node: string;
|
|
835
|
+
phase: NodePhase;
|
|
836
|
+
outcome?: Outcome;
|
|
837
|
+
} | {
|
|
838
|
+
kind: 'dag:end';
|
|
839
|
+
ts: number;
|
|
840
|
+
path: string[];
|
|
841
|
+
outcome: Outcome;
|
|
842
|
+
} | {
|
|
843
|
+
kind: 'dag:kickback';
|
|
844
|
+
ts: number;
|
|
845
|
+
path: string[];
|
|
846
|
+
from: string;
|
|
847
|
+
to: string;
|
|
848
|
+
reason: string;
|
|
849
|
+
accepted: boolean;
|
|
850
|
+
note?: string;
|
|
851
|
+
} | {
|
|
852
|
+
kind: 'job:start';
|
|
853
|
+
ts: number;
|
|
854
|
+
path: string[];
|
|
855
|
+
label: string;
|
|
856
|
+
} | {
|
|
857
|
+
kind: 'job:end';
|
|
858
|
+
ts: number;
|
|
859
|
+
path: string[];
|
|
860
|
+
label: string;
|
|
861
|
+
outcome: Outcome;
|
|
862
|
+
} | {
|
|
863
|
+
kind: 'engine:text';
|
|
864
|
+
ts: number;
|
|
865
|
+
path: string[];
|
|
866
|
+
delta: string;
|
|
867
|
+
} | {
|
|
868
|
+
kind: 'engine:thinking';
|
|
869
|
+
ts: number;
|
|
870
|
+
path: string[];
|
|
871
|
+
delta: string;
|
|
872
|
+
} | {
|
|
873
|
+
kind: 'engine:tool';
|
|
874
|
+
ts: number;
|
|
875
|
+
path: string[];
|
|
876
|
+
name: string;
|
|
877
|
+
phase: 'use' | 'result';
|
|
878
|
+
} | {
|
|
879
|
+
kind: 'engine:usage';
|
|
880
|
+
ts: number;
|
|
881
|
+
path: string[];
|
|
882
|
+
model: string;
|
|
883
|
+
usage: Usage;
|
|
884
|
+
} | {
|
|
885
|
+
kind: 'log';
|
|
886
|
+
ts: number;
|
|
887
|
+
path: string[];
|
|
888
|
+
level: LogLevel;
|
|
889
|
+
message: string;
|
|
890
|
+
} | {
|
|
891
|
+
kind: 'error';
|
|
892
|
+
ts: number;
|
|
893
|
+
path: string[];
|
|
894
|
+
message: string;
|
|
895
|
+
code: string;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
export { commitJob as $, type AgentDef as A, type BudgetConfig as B, type ConditionInput as C, type DagConfig as D, type Environment as E, type Forge as F, GhForge as G, type OutcomeStatus as H, type PrPatch as I, type Job as J, type PrRef as K, type LoopConfig as L, type MergeOptions as M, type RetryPolicy as N, type Outcome as O, type PrInput as P, type Skill as Q, type RawPredicate as R, SUBAGENT_TOOLS as S, agentJob as T, type Usage as U, buildChecksArgs as V, type Workspace as W, buildCreateArgs as X, buildEditArgs as Y, buildMergeArgs as Z, buildViewArgs as _, type JobContext as a, defineAgent as a0, defineSkill as a1, fnJob as a2, fromFile as a3, isEngine as a4, isEnvironment as a5, isForge as a6, kickback as a7, resolveSystem as a8, type JobMeta as b, type EngineRef as c, type Condition as d, type EngineOptions as e, type Engine as f, type EngineName as g, type AgentRequest as h, type EngineEventSink as i, type AgentResult as j, type EnvHandle as k, type LoopEvent as l, type LimitPolicy as m, type AgentJobConfig as n, Budget as o, type CommitJobConfig as p, type ConditionResult as q, type DagNode as r, type EngineStreamEvent as s, type ForgeOpts as t, type GroundConfig as u, type LogLevel as v, LoopError as w, type LoopErrorCode as x, MockForge as y, type MockForgeOptions as z };
|