@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
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
import { L as LoopConfig, J as Job, D as DagConfig, O as Outcome, a as JobContext, C as ConditionInput, b as JobMeta, c as EngineRef, W as Workspace, A as AgentDef, d as Condition, e as EngineOptions, f as Engine, g as EngineName, h as AgentRequest, U as Usage, i as EngineEventSink, j as AgentResult, E as Environment, k as EnvHandle, l as LoopEvent, F as Forge, B as BudgetConfig, m as LimitPolicy } from './types-B4wGVpqo.js';
|
|
2
|
+
export { n as AgentJobConfig, o as Budget, p as CommitJobConfig, q as ConditionResult, r as DagNode, s as EngineStreamEvent, t as ForgeOpts, G as GhForge, u as GroundConfig, v as LogLevel, w as LoopError, x as LoopErrorCode, M as MergeOptions, y as MockForge, z as MockForgeOptions, H as OutcomeStatus, P as PrInput, I as PrPatch, K as PrRef, R as RawPredicate, N as RetryPolicy, S as SUBAGENT_TOOLS, Q as Skill, T as agentJob, V as buildChecksArgs, X as buildCreateArgs, Y as buildEditArgs, Z as buildMergeArgs, _ as buildViewArgs, $ as commitJob, a0 as defineAgent, a1 as defineSkill, a2 as fnJob, a3 as fromFile, a4 as isEngine, a5 as isEnvironment, a6 as isForge, a7 as kickback, a8 as resolveSystem } from './types-B4wGVpqo.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The loop primitive. `loop(config)` returns a `Job`, so loops nest by simply
|
|
6
|
+
* passing one as another's `body` or `review`.
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle of one loop:
|
|
9
|
+
* 1. `start` gate (one-or-many conditions) — unmet => `aborted`.
|
|
10
|
+
* 2. repeat, up to `max`:
|
|
11
|
+
* run `body` (fresh context each turn) → `stopOn`? → `until`?
|
|
12
|
+
* if `until` is met and there's a `review`, run it:
|
|
13
|
+
* review `pass` => loop completes `pass`
|
|
14
|
+
* review !pass => re-enter the loop ← "review fails, run main loop again"
|
|
15
|
+
* with no `until`, a `pass` body ends the loop; `max` reached => `exhausted`.
|
|
16
|
+
* 3. `onComplete` post-action runs once, whatever the status.
|
|
17
|
+
*
|
|
18
|
+
* The review-restart cycle is bounded: by `max` (shared with ordinary
|
|
19
|
+
* iterations) and, independently, by `maxReviewRestarts`. The failed review is
|
|
20
|
+
* threaded to the next iteration as `ctx.lastReview` so the body can act on it.
|
|
21
|
+
*
|
|
22
|
+
* Every piece of user code (conditions, the body, hooks, the review) is guarded:
|
|
23
|
+
* a throw is classified and ends the loop with `fail`, but `loop:end` and the
|
|
24
|
+
* `onComplete` post-action still run.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
declare function loop(config: LoopConfig): Job;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The DAG / stages layer. `dag(config)` returns a `Job`, so it nests with
|
|
31
|
+
* `loop()` both ways. Nodes declare `needs` (dependencies); each node waits on
|
|
32
|
+
* its dependencies' promises, then runs under a shared `p-limit` concurrency
|
|
33
|
+
* gate. Cycle/missing-dep detection is delegated to `toposort` and happens
|
|
34
|
+
* before any work runs.
|
|
35
|
+
*
|
|
36
|
+
* Failure policy (ours, not the libs'):
|
|
37
|
+
* - a required node failing blocks its dependents (they don't run);
|
|
38
|
+
* - with `stopOnError` (default) the first required failure stops scheduling
|
|
39
|
+
* anything not already in flight;
|
|
40
|
+
* - `optional` nodes never fail the DAG nor block dependents;
|
|
41
|
+
* - an unmet `when` gate *skips* the node, which counts as green.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
declare function dag(config: DagConfig): Job;
|
|
45
|
+
/** Run jobs strictly in order; stop at the first non-pass. Sugar over `dag`. */
|
|
46
|
+
declare function sequence(name: string, ...jobs: Job[]): Job;
|
|
47
|
+
/** Run jobs concurrently (optionally capped); all run regardless of failures. */
|
|
48
|
+
declare function parallel(name: string, jobs: Record<string, Job> | Job[], concurrency?: number): Job;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Tournament — branch-and-select (GCC's `BRANCH` + pick-the-winner). Where a DAG
|
|
52
|
+
* fans out DISJOINT work and lands all of it, a tournament explores ALTERNATIVE
|
|
53
|
+
* approaches to the SAME task: run N candidates, each in its own isolated
|
|
54
|
+
* worktree, judge them, land only the winner and discard the rest.
|
|
55
|
+
*
|
|
56
|
+
* It is a thin composition over the worktree primitives — no new machinery. Only
|
|
57
|
+
* one branch is ever merged (the winner, off an unchanged HEAD), so there is no
|
|
58
|
+
* conflict to resolve. Small on purpose.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
interface TournamentConfig {
|
|
62
|
+
name: string;
|
|
63
|
+
/** Candidates to run. */
|
|
64
|
+
n: number;
|
|
65
|
+
/** Build the candidate job for attempt `i` (0-based) — same task, varied angle. */
|
|
66
|
+
candidate: (i: number) => Job;
|
|
67
|
+
/**
|
|
68
|
+
* Score a finished candidate (higher wins). Run against the candidate's own
|
|
69
|
+
* context, so it can read the candidate's workspace. The highest-scoring
|
|
70
|
+
* passing candidate lands back; ties break to the earliest.
|
|
71
|
+
*/
|
|
72
|
+
judge: (outcome: Outcome, ctx: JobContext) => number | Promise<number>;
|
|
73
|
+
/** Max candidates running at once. Default `n`. */
|
|
74
|
+
concurrency?: number;
|
|
75
|
+
}
|
|
76
|
+
declare function tournament(config: TournamentConfig): Job;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Job introspection. The builders register a `JobMeta` for the `Job` they return
|
|
80
|
+
* (and a short label for the conditions they build) in a side table, so a loop's
|
|
81
|
+
* shape can be read back without running it. This is what powers `loops validate`
|
|
82
|
+
* and `loops describe`: an agent authors a loop, then sees what it actually built.
|
|
83
|
+
*
|
|
84
|
+
* Kept in `WeakMap`s rather than on the function objects, so the `Job`/`Condition`
|
|
85
|
+
* types stay plain functions and nothing downstream has to know meta exists.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/** Read a Job's registered shape, if it has one (a hand-written Job has none). */
|
|
89
|
+
declare function jobMeta(job: Job): JobMeta | undefined;
|
|
90
|
+
/** Flatten a gate input (`until`/`start`/`stopOn`) into one label per condition. */
|
|
91
|
+
declare function describeConditions(input?: ConditionInput): string[];
|
|
92
|
+
/**
|
|
93
|
+
* Render a `JobMeta` tree to indented lines: the loop's name and cap, its gate
|
|
94
|
+
* and convergence actions, and its body / dag nodes recursively. A Job with no
|
|
95
|
+
* meta (hand-written) renders as a single opaque line.
|
|
96
|
+
*/
|
|
97
|
+
declare function renderPlan(meta: JobMeta | undefined, indent?: string): string[];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The git substrate. loops' answer to cross-iteration amnesia is to make the
|
|
101
|
+
* commit log the convergence ledger: each unit of work commits the "way" (a
|
|
102
|
+
* structured body) welded to the "what" (the diff), and the next fresh context
|
|
103
|
+
* reads the log back. This module is the thin, engine-agnostic wrapper that lets
|
|
104
|
+
* the core do that — every function takes an explicit `cwd` (the worktree dir)
|
|
105
|
+
* and never throws for an expected "no" answer.
|
|
106
|
+
*
|
|
107
|
+
* It is deliberately small: a handful of plumbing/porcelain calls over `execa`,
|
|
108
|
+
* the same subprocess primitive `commandSucceeds` already uses. No git library,
|
|
109
|
+
* no parallel state. Git is the state.
|
|
110
|
+
*/
|
|
111
|
+
/** One commit as the ledger sees it: the what (sha) plus the way (body). */
|
|
112
|
+
interface CommitRecord {
|
|
113
|
+
sha: string;
|
|
114
|
+
/** Conventional-commit subject line. */
|
|
115
|
+
subject: string;
|
|
116
|
+
/** The structured body (the "way") — everything after the subject. */
|
|
117
|
+
body: string;
|
|
118
|
+
/** ISO author date. */
|
|
119
|
+
date: string;
|
|
120
|
+
}
|
|
121
|
+
interface GitOpts {
|
|
122
|
+
cwd: string;
|
|
123
|
+
signal?: AbortSignal;
|
|
124
|
+
}
|
|
125
|
+
/** True when `cwd` is inside a git work tree. Never throws. */
|
|
126
|
+
declare function isRepo(opts: GitOpts): Promise<boolean>;
|
|
127
|
+
/** The checked-out branch name, or undefined on a detached HEAD / non-repo. */
|
|
128
|
+
declare function currentBranch(opts: GitOpts): Promise<string | undefined>;
|
|
129
|
+
/** The HEAD commit sha, or undefined when the branch has no commits yet. */
|
|
130
|
+
declare function headSha(opts: GitOpts): Promise<string | undefined>;
|
|
131
|
+
/** Stage every change in the work tree (`git add -A`). */
|
|
132
|
+
declare function stageAll(opts: GitOpts): Promise<void>;
|
|
133
|
+
/** True when there is something staged to commit. */
|
|
134
|
+
declare function hasStagedChanges(opts: GitOpts): Promise<boolean>;
|
|
135
|
+
/** True when the work tree (staged or unstaged) has any change. */
|
|
136
|
+
declare function isDirty(opts: GitOpts): Promise<boolean>;
|
|
137
|
+
interface CommitInput {
|
|
138
|
+
subject: string;
|
|
139
|
+
/** The structured body — the "way". Joined to the subject with a blank line. */
|
|
140
|
+
body?: string;
|
|
141
|
+
/** Commit even with an empty index (default false). */
|
|
142
|
+
allowEmpty?: boolean;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Commit the staged index. The message is passed on stdin (`-F -`) so an
|
|
146
|
+
* arbitrarily-shaped body never has to survive shell escaping. The repo's
|
|
147
|
+
* configured author is used — loops never sets an author or a co-author trailer.
|
|
148
|
+
* Returns the new sha, or undefined when there was nothing to commit and
|
|
149
|
+
* `allowEmpty` was not set.
|
|
150
|
+
*/
|
|
151
|
+
declare function commit(input: CommitInput, opts: GitOpts): Promise<string | undefined>;
|
|
152
|
+
interface LogQuery extends GitOpts {
|
|
153
|
+
/** Exclusive lower bound: only commits after this ref (e.g. the loop start). */
|
|
154
|
+
since?: string;
|
|
155
|
+
/** Cap the number of commits returned (most recent first). */
|
|
156
|
+
max?: number;
|
|
157
|
+
/** The ref to read (default HEAD) — e.g. a fork branch's own line of work. */
|
|
158
|
+
ref?: string;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Read the ledger: recent commits, newest first, each with its body (the way).
|
|
162
|
+
* `since` gives the "this run only" window the loop grounds the next iteration
|
|
163
|
+
* on; `max` bounds it so the ledger never re-rots the fresh context.
|
|
164
|
+
*/
|
|
165
|
+
declare function log(query: LogQuery): Promise<CommitRecord[]>;
|
|
166
|
+
interface WorktreeHandle {
|
|
167
|
+
/** The isolated working directory. */
|
|
168
|
+
dir: string;
|
|
169
|
+
/** The branch checked out there. */
|
|
170
|
+
branch: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Fork an isolated worktree on a new branch from `base` (default HEAD). This is
|
|
174
|
+
* how a concurrency boundary becomes a team: each concurrent writer gets its own
|
|
175
|
+
* working dir and branch, so siblings never collide on files or the index.
|
|
176
|
+
*/
|
|
177
|
+
declare function addWorktree(repoDir: string, opts: {
|
|
178
|
+
branch: string;
|
|
179
|
+
base?: string;
|
|
180
|
+
signal?: AbortSignal;
|
|
181
|
+
}): Promise<WorktreeHandle>;
|
|
182
|
+
/** Remove a worktree (force-discards anything uncommitted left in it). */
|
|
183
|
+
declare function removeWorktree(repoDir: string, dir: string, opts?: {
|
|
184
|
+
signal?: AbortSignal;
|
|
185
|
+
}): Promise<void>;
|
|
186
|
+
/** Delete a branch ref (used to clean up a merged fork branch). */
|
|
187
|
+
declare function deleteBranch(repoDir: string, branch: string, opts?: {
|
|
188
|
+
signal?: AbortSignal;
|
|
189
|
+
}): Promise<void>;
|
|
190
|
+
interface MergeResult {
|
|
191
|
+
ok: boolean;
|
|
192
|
+
conflict: boolean;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Land a fork branch back into the branch checked out at `repoDir`, preserving
|
|
196
|
+
* the team shape (`--no-ff`). On conflict the merge is aborted so the target
|
|
197
|
+
* stays clean and the caller can fail the node honestly — loops does not
|
|
198
|
+
* auto-resolve (a merge-resolver is a separate, later layer).
|
|
199
|
+
*/
|
|
200
|
+
declare function mergeBranch(repoDir: string, branch: string, opts?: {
|
|
201
|
+
signal?: AbortSignal;
|
|
202
|
+
message?: string;
|
|
203
|
+
}): Promise<MergeResult>;
|
|
204
|
+
/**
|
|
205
|
+
* Begin a `--no-ff --no-commit` merge WITHOUT aborting on conflict, so a resolver
|
|
206
|
+
* can synthesise the result. `clean` means it merged cleanly (staged, ready to
|
|
207
|
+
* commit); otherwise `conflicted` lists the unresolved paths (with markers).
|
|
208
|
+
*/
|
|
209
|
+
declare function mergeNoCommit(repoDir: string, branch: string, opts?: {
|
|
210
|
+
signal?: AbortSignal;
|
|
211
|
+
}): Promise<{
|
|
212
|
+
clean: boolean;
|
|
213
|
+
conflicted: string[];
|
|
214
|
+
}>;
|
|
215
|
+
/** Paths with unresolved merge conflicts. */
|
|
216
|
+
declare function conflictedFiles(repoDir: string, opts?: {
|
|
217
|
+
signal?: AbortSignal;
|
|
218
|
+
}): Promise<string[]>;
|
|
219
|
+
/** Abort an in-progress merge. */
|
|
220
|
+
declare function mergeAbort(repoDir: string, opts?: {
|
|
221
|
+
signal?: AbortSignal;
|
|
222
|
+
}): Promise<void>;
|
|
223
|
+
interface PushOptions extends GitOpts {
|
|
224
|
+
/** The remote to push to. Default `origin`. */
|
|
225
|
+
remote?: string;
|
|
226
|
+
/** The branch to push. Default the branch checked out at `cwd`. */
|
|
227
|
+
branch?: string;
|
|
228
|
+
/** Set the upstream tracking ref (`-u`). Default true. */
|
|
229
|
+
setUpstream?: boolean;
|
|
230
|
+
/** Force-with-lease the push. Default false. */
|
|
231
|
+
force?: boolean;
|
|
232
|
+
}
|
|
233
|
+
interface PushResult {
|
|
234
|
+
ok: boolean;
|
|
235
|
+
/** The combined git output, surfaced on failure (no remote, rejected, etc.). */
|
|
236
|
+
output: string;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Push the branch to a remote — the one place loops reaches past the local
|
|
240
|
+
* substrate. Honest like the rest of this module: a non-zero exit (no remote, a
|
|
241
|
+
* rejected non-fast-forward, no upstream) comes back as `{ ok: false, output }`
|
|
242
|
+
* for the caller to surface, never a throw. `--force-with-lease` is the only
|
|
243
|
+
* force offered, so a force push still refuses to clobber unseen remote work.
|
|
244
|
+
*/
|
|
245
|
+
declare function push(opts: PushOptions): Promise<PushResult>;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Merge as synthesis (GCC's `MERGE`). A raw `git merge` either applies cleanly or
|
|
249
|
+
* fails on conflict. `mergeSynthesis` does the thing a teammate does: when two
|
|
250
|
+
* lines of work collide, an agent RESOLVES each conflicted file coherently
|
|
251
|
+
* (preserving both intents), and the merge commit body is a SYNTHESIS of what the
|
|
252
|
+
* two branches were each trying to do — not "merge branch X".
|
|
253
|
+
*
|
|
254
|
+
* It is text-in/text-out, so it works through any `Engine` (no tool-use needed):
|
|
255
|
+
* the conflicted file content goes in, the resolved content comes back. Light: a
|
|
256
|
+
* call per conflicted file plus one for the synthesis body, and nothing when the
|
|
257
|
+
* merge is already clean. The merge is aborted if resolution throws, so the target
|
|
258
|
+
* is never left half-merged.
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
interface MergeSynthesisConfig {
|
|
262
|
+
/** The branch to land into the current workspace. */
|
|
263
|
+
branch: string;
|
|
264
|
+
/** Conventional subject for the merge commit. */
|
|
265
|
+
message?: string;
|
|
266
|
+
engine?: EngineRef;
|
|
267
|
+
model?: string;
|
|
268
|
+
}
|
|
269
|
+
interface MergeSynthesisResult {
|
|
270
|
+
ok: boolean;
|
|
271
|
+
/** Whether a conflict had to be resolved. */
|
|
272
|
+
conflict: boolean;
|
|
273
|
+
sha?: string;
|
|
274
|
+
}
|
|
275
|
+
declare function mergeSynthesis(ctx: JobContext, config: MergeSynthesisConfig): Promise<MergeSynthesisResult>;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Pull-request jobs — how a converged branch becomes a PR whose body stays a
|
|
279
|
+
* faithful synthesis of the work, so the commit-log memory survives a squash merge.
|
|
280
|
+
*
|
|
281
|
+
* The squash-merge problem: a branch carries N milestone commits, each with a rich
|
|
282
|
+
* structured "way" (the Ledger). A squash merge collapses them into one commit whose
|
|
283
|
+
* body GitHub defaults to a list of subject lines — the reasoning is lost from the
|
|
284
|
+
* base branch's history. The fix is small because loops already folds many commit
|
|
285
|
+
* bodies into one: `pullRequestJob` sets the PR body to `consolidate(since: base)` —
|
|
286
|
+
* the same decision-preserving fold, scoped to this branch — and keeps it current.
|
|
287
|
+
* `mergeJob` can then squash with that synthesis as the commit body directly.
|
|
288
|
+
*
|
|
289
|
+
* Engine-agnostic and host-agnostic: the host is the injectable `Forge` seam
|
|
290
|
+
* (default `GhForge`), so these jobs run offline against a `MockForge` in tests.
|
|
291
|
+
*/
|
|
292
|
+
|
|
293
|
+
type Derive<T> = T | ((ctx: JobContext, last: Outcome | undefined) => T | Promise<T>);
|
|
294
|
+
interface PushJobConfig {
|
|
295
|
+
label?: string;
|
|
296
|
+
/** Remote to push to. Default `origin`. */
|
|
297
|
+
remote?: string;
|
|
298
|
+
/** Branch to push. Default the workspace branch. */
|
|
299
|
+
branch?: string;
|
|
300
|
+
/** Set upstream tracking. Default true. */
|
|
301
|
+
setUpstream?: boolean;
|
|
302
|
+
/** Force-with-lease. Default false. */
|
|
303
|
+
force?: boolean;
|
|
304
|
+
}
|
|
305
|
+
/** Push the work branch to its remote. Idempotent; a rejected push fails honestly. */
|
|
306
|
+
declare function pushJob(config?: PushJobConfig): Job;
|
|
307
|
+
interface PullRequestJobConfig {
|
|
308
|
+
label?: string;
|
|
309
|
+
/** PR title, or a function of context. Default: the converged outcome summary. */
|
|
310
|
+
title?: Derive<string>;
|
|
311
|
+
/** Base branch to merge into, and the `since` bound for the body fold. Default `main`. */
|
|
312
|
+
base?: string;
|
|
313
|
+
/** Push the branch first (idempotent). Default true; pass a config to tune. */
|
|
314
|
+
push?: boolean | PushJobConfig;
|
|
315
|
+
/** Open as a draft when first created. */
|
|
316
|
+
draft?: boolean;
|
|
317
|
+
/** Model for the body consolidation call. */
|
|
318
|
+
model?: string;
|
|
319
|
+
/** Max milestones to fold into the body. Default 50. */
|
|
320
|
+
max?: number;
|
|
321
|
+
/** Override the synthesized body (else: consolidate the branch's commit bodies). */
|
|
322
|
+
body?: Derive<string>;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Raise the PR, or update it if it already exists, with a body synthesized from the
|
|
326
|
+
* branch's commit bodies. Idempotent create-or-update: run it after each milestone
|
|
327
|
+
* (or at convergence) and the PR description stays current — that is what keeps the
|
|
328
|
+
* eventual squash body honest. Returns the `PrRef` in `outcome.data.pr`.
|
|
329
|
+
*/
|
|
330
|
+
declare function pullRequestJob(config?: PullRequestJobConfig): Job;
|
|
331
|
+
interface MergeJobConfig {
|
|
332
|
+
label?: string;
|
|
333
|
+
/** Base branch — the `since` bound for re-synthesizing the squash body. Default `main`. */
|
|
334
|
+
base?: string;
|
|
335
|
+
/** Squash merge (default true) — the whole reason this exists. */
|
|
336
|
+
squash?: boolean;
|
|
337
|
+
/**
|
|
338
|
+
* Hand the merge to GitHub auto-merge (`gh pr merge --auto`): it lands once the
|
|
339
|
+
* required checks pass. The recommended "merge when CI is green" path — non-blocking.
|
|
340
|
+
*/
|
|
341
|
+
auto?: boolean;
|
|
342
|
+
/** Delete the head branch after merge. */
|
|
343
|
+
deleteBranch?: boolean;
|
|
344
|
+
/** Squash commit subject. */
|
|
345
|
+
subject?: Derive<string>;
|
|
346
|
+
/** Squash commit body. Default: re-consolidate the branch (the up-to-date synthesis). */
|
|
347
|
+
body?: Derive<string>;
|
|
348
|
+
model?: string;
|
|
349
|
+
max?: number;
|
|
350
|
+
/**
|
|
351
|
+
* A gate that must hold before loops issues the merge — e.g. `forgeChecks()` for a
|
|
352
|
+
* synchronous "CI is green" check, or any `Condition`. Unmet → the job fails without
|
|
353
|
+
* merging. (For the non-blocking path, prefer `auto: true` and let GitHub gate.)
|
|
354
|
+
*/
|
|
355
|
+
when?: ConditionInput;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Squash-merge the branch's PR with a body synthesized from its commit bodies — so the
|
|
359
|
+
* one commit that lands on the base branch carries the whole "way", not a list of
|
|
360
|
+
* subjects. Opt-in (loops performing an outward merge is high-stakes): gate it with
|
|
361
|
+
* `auto: true` (GitHub merges when checks pass) and/or a `when` condition.
|
|
362
|
+
*/
|
|
363
|
+
declare function mergeJob(config?: MergeJobConfig): Job;
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* `isolated(job)` — run any Job in its own git worktree on a fork branch, and land
|
|
367
|
+
* its work back into the parent branch on pass. The concurrency boundary as a Job
|
|
368
|
+
* WRAPPER, not a node type.
|
|
369
|
+
*
|
|
370
|
+
* dag nodes can already fork a worktree (`isolation: 'worktree'`), but that only
|
|
371
|
+
* works for predeclared nodes. A Tend loop dispatches DYNAMICALLY — it discovers
|
|
372
|
+
* each ticket at runtime and routes it to the right shape of sub-loop — and each
|
|
373
|
+
* dispatch wants its own isolated worktree so parallel tickets never collide on
|
|
374
|
+
* files or the index. `isolated()` makes that composable: wrap the dispatched Job.
|
|
375
|
+
*
|
|
376
|
+
* On pass: any uncommitted remainder is committed in the worktree, then the fork
|
|
377
|
+
* branch merges back (`--no-ff`). Land-back merges are serialised across all
|
|
378
|
+
* `isolated()` jobs in the process, so concurrent dispatch cannot race the parent
|
|
379
|
+
* index/HEAD. A conflict fails honestly, or is synthesised when asked. The worktree
|
|
380
|
+
* is always removed; a cleanly-merged fork branch is deleted. A non-repo workspace
|
|
381
|
+
* degrades to running in place (a warning, no isolation).
|
|
382
|
+
*
|
|
383
|
+
* NOTE: dag's own runNodeJob holds parallel worktree/land-back logic (plus per-team
|
|
384
|
+
* environments). The two should be unified — dag delegating to `isolated()` — once
|
|
385
|
+
* `isolated()` grows environment support; until then the land-back logic lives in
|
|
386
|
+
* both deliberately, to avoid destabilising the dag path.
|
|
387
|
+
*/
|
|
388
|
+
|
|
389
|
+
interface IsolatedOptions {
|
|
390
|
+
/** Label for the fork branch and the child path. Default 'isolated'. */
|
|
391
|
+
label?: string;
|
|
392
|
+
/** On a land-back conflict: 'fail' (default) or 'synthesize'. */
|
|
393
|
+
onConflict?: 'fail' | 'synthesize';
|
|
394
|
+
}
|
|
395
|
+
/** Wrap a Job so it runs in an isolated worktree and lands back on pass. */
|
|
396
|
+
declare function isolated(job: Job, opts?: IsolatedOptions): Job;
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The scratch files (`.loops/`) — two transient write-ahead buffers that carry a
|
|
400
|
+
* unit of work's memory forward, split by AUDIENCE:
|
|
401
|
+
*
|
|
402
|
+
* - `ledger.md` is WORKING MEMORY, for the agent(s) doing the work NOW. Verbose and
|
|
403
|
+
* real-time: the running log of what is being tried, for the agent itself and for
|
|
404
|
+
* any concurrent peers fanned out on the same team. The harness appends to it
|
|
405
|
+
* automatically after each turn (auto-capture), so the why is recorded even when a
|
|
406
|
+
* single agent's context decays or no one holds all the reasoning at the end.
|
|
407
|
+
*
|
|
408
|
+
* - `prompt.md` is the HANDOFF, for the NEXT agent(s). Distilled and curated: the
|
|
409
|
+
* why, what was ruled out, the constraints, what is left. Grounding injects it
|
|
410
|
+
* into the next context as the start of its prompt; `commitJob` crystallises it
|
|
411
|
+
* into the commit body (alongside a compacted ledger). Same artifact, two stages.
|
|
412
|
+
*
|
|
413
|
+
* The commit body is `prompt.md` + a compacted `ledger.md`, welded to its diff — and
|
|
414
|
+
* it does not expire at the next turn. It is a permanent record in git history that
|
|
415
|
+
* ANY later agent can look back to, as far back as it wants: recent-N grounding
|
|
416
|
+
* surfaces the nearby ones, retrieval selects the relevant ones however old, and an
|
|
417
|
+
* agent can always walk the log itself. Each one says "here is how to reason about
|
|
418
|
+
* this snapshot of changes" — the why, what was ruled out, the constraints, and what
|
|
419
|
+
* the implementer actually did. Both files reset once the commit lands (crystallise,
|
|
420
|
+
* then reset); the record they became lives on in the history.
|
|
421
|
+
*
|
|
422
|
+
* The whole `.loops/` dir is kept out of git (self-managed `.gitignore`), so
|
|
423
|
+
* `commitJob`'s `git add -A` never stages either file. The files are the draft; the
|
|
424
|
+
* commit is the record.
|
|
425
|
+
*/
|
|
426
|
+
|
|
427
|
+
/** Absolute path to a workspace's working memory (`ledger.md`). */
|
|
428
|
+
declare function ledgerPath(workspace: Workspace): string;
|
|
429
|
+
/** Absolute path to a workspace's handoff (`prompt.md`, the staged commit body). */
|
|
430
|
+
declare function promptPath(workspace: Workspace): string;
|
|
431
|
+
/**
|
|
432
|
+
* Guarantee `.loops/.gitignore` ignores everything, so neither scratch file is ever
|
|
433
|
+
* staged — even if an agent wrote one directly rather than through these helpers.
|
|
434
|
+
* No-op when `.loops/` does not exist.
|
|
435
|
+
*/
|
|
436
|
+
declare function ensureIgnored(workspace: Workspace): void;
|
|
437
|
+
interface PromptNote {
|
|
438
|
+
/** Optional section heading (Why / Alternatives / Constraints / Next…). */
|
|
439
|
+
heading?: string;
|
|
440
|
+
/** The reasoning to record — the "why". */
|
|
441
|
+
body: string;
|
|
442
|
+
/** Who recorded it, so a fanned-out team's why stays attributable. */
|
|
443
|
+
author?: string;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Append a note to the handoff. Durable and append-only: many agents on one team
|
|
447
|
+
* add to the same handoff as they work, and the order is preserved. Uses an
|
|
448
|
+
* O_APPEND write, so concurrent appends do not clobber each other.
|
|
449
|
+
*/
|
|
450
|
+
declare function appendPrompt(workspace: Workspace, note: PromptNote | string): void;
|
|
451
|
+
/** Read the handoff, or '' when nothing has been drafted. */
|
|
452
|
+
declare function readPrompt(workspace: Workspace): string;
|
|
453
|
+
/** Clear the handoff at the commit boundary (the atomicity rule: then reset). */
|
|
454
|
+
declare function resetPrompt(workspace: Workspace): void;
|
|
455
|
+
interface LedgerEntry {
|
|
456
|
+
/** The job/agent label for the turn. */
|
|
457
|
+
label?: string;
|
|
458
|
+
/** The iteration number, when inside a loop. */
|
|
459
|
+
iteration?: number;
|
|
460
|
+
/** The agent's own reasoning text for the turn. */
|
|
461
|
+
text?: string;
|
|
462
|
+
/** Tool actions taken this turn, pre-summarised (e.g. `['Edit×2', 'Bash']`). */
|
|
463
|
+
tools?: string[];
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Append a turn to the working memory. This is the auto-capture sink: the harness
|
|
467
|
+
* records each grounded turn here (reasoning + a summary of actions), and agents can
|
|
468
|
+
* jot their own notes too. Append-only and O_APPEND, so concurrent peers don't
|
|
469
|
+
* clobber each other.
|
|
470
|
+
*/
|
|
471
|
+
declare function appendLedger(workspace: Workspace, entry: LedgerEntry | string): void;
|
|
472
|
+
/** Read the working memory, or '' when nothing has been logged. */
|
|
473
|
+
declare function readLedger(workspace: Workspace): string;
|
|
474
|
+
/** Clear the working memory at the commit boundary. */
|
|
475
|
+
declare function resetLedger(workspace: Workspace): void;
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The read side — grounding. Where the draft carries the within-iteration why,
|
|
479
|
+
* grounding carries the cross-iteration memory: before a fresh context does
|
|
480
|
+
* work, it reads the recent commit log so it knows what prior iterations already
|
|
481
|
+
* tried and why, and does not re-walk a dead end. This is the half of the
|
|
482
|
+
* fresh-context bet that kills amnesia (the other half being that fresh context
|
|
483
|
+
* kills rot).
|
|
484
|
+
*
|
|
485
|
+
* The reach is deliberately BRANCH-LOCAL. `git log` on the current branch is the
|
|
486
|
+
* committed truth of this line of work; adjacent active branches are in-flight
|
|
487
|
+
* and may never land, so grounding on them feeds the agent premises that can
|
|
488
|
+
* vanish. When a sibling team's work matters, it lands back into this line and
|
|
489
|
+
* grounding then picks it up naturally — the merge is where work becomes shared
|
|
490
|
+
* truth. Cross-branch awareness, if ever wanted, is a separate, thin, opt-in
|
|
491
|
+
* signal, not this read.
|
|
492
|
+
*/
|
|
493
|
+
|
|
494
|
+
interface GroundOptions {
|
|
495
|
+
/**
|
|
496
|
+
* Exclusive lower bound: only commits after this ref. Pass the loop's start
|
|
497
|
+
* ref to scope the ledger to "this run". Omitted reads recent branch history.
|
|
498
|
+
*/
|
|
499
|
+
since?: string;
|
|
500
|
+
/** Max commits to include (newest first). Default 10. */
|
|
501
|
+
max?: number;
|
|
502
|
+
/** Truncate each commit body to this many chars, so the ledger never re-rots
|
|
503
|
+
* the fresh context. Default 1200. */
|
|
504
|
+
bodyChars?: number;
|
|
505
|
+
signal?: AbortSignal;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Render the recent ledger as a prompt block for the next fresh context. Returns
|
|
509
|
+
* '' when there is nothing yet (a first iteration on a fresh branch), so callers
|
|
510
|
+
* can prepend it unconditionally. Newest commit first.
|
|
511
|
+
*/
|
|
512
|
+
declare function groundingText(workspace: Workspace, opts?: GroundOptions): Promise<string>;
|
|
513
|
+
interface RetrieveOptions {
|
|
514
|
+
/** The task/intent to find relevant prior commits for. */
|
|
515
|
+
intent: string;
|
|
516
|
+
/**
|
|
517
|
+
* The recall window: how many recent commits to offer the selector as
|
|
518
|
+
* candidates. A relevant commit OLDER than this is invisible — retrieval is not
|
|
519
|
+
* unbounded, it just has a bigger window than recent-N. Reading subjects is
|
|
520
|
+
* cheap, so this can be generous. For a log longer than any practical window,
|
|
521
|
+
* run consolidation: the consolidated-ledger commit stays in-window and indexes the
|
|
522
|
+
* old history. Default 100.
|
|
523
|
+
*/
|
|
524
|
+
candidates?: number;
|
|
525
|
+
/** Max commits to inject. Default 8. */
|
|
526
|
+
max?: number;
|
|
527
|
+
/** Truncate each injected body. Default 1200. */
|
|
528
|
+
bodyChars?: number;
|
|
529
|
+
/** Engine for the (cheap) selection call. Default the run engine. */
|
|
530
|
+
engine?: EngineRef;
|
|
531
|
+
/** Model for the selection call (a small one is plenty). */
|
|
532
|
+
model?: string;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Render only the prior commits a cheap model judges relevant to `intent`.
|
|
536
|
+
* Returns '' when nothing is on the branch or nothing is judged relevant.
|
|
537
|
+
*/
|
|
538
|
+
declare function retrieveLedger(ctx: JobContext, opts: RetrieveOptions): Promise<string>;
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Consolidation — the "sleep-time" step (Letta's reflection, DiffMem's consolidate).
|
|
542
|
+
* A long run accumulates many milestone commits; consolidation folds them into one
|
|
543
|
+
* bounded CONSOLIDATED LEDGER: the current state, the open threads, and every binding
|
|
544
|
+
* decision preserved. It is the COARSE tier of the ledger — `ledger.md` is the fine
|
|
545
|
+
* tier and the milestone commit bodies are the mid tier, so multi-granularity falls
|
|
546
|
+
* out of git rather than a new artifact to maintain.
|
|
547
|
+
*
|
|
548
|
+
* It is decision-PRESERVING, not a progress summary: a fresh context must be able to
|
|
549
|
+
* honour every convention and constraint the project settled, so consolidation keeps
|
|
550
|
+
* exact values verbatim while dropping narrative. (A naive summary that compresses
|
|
551
|
+
* the decisions away lets downstream work silently violate them — measured: top-k
|
|
552
|
+
* retrieval and a progress summary both miss accrued decisions; only a decision-
|
|
553
|
+
* preserving ledger keeps them in bounded space.)
|
|
554
|
+
*
|
|
555
|
+
* The consolidated ledger is a COMMIT BODY, not a tracked file — the same shape as
|
|
556
|
+
* every other memory in loops (welded to a diff, read back by grounding). Each
|
|
557
|
+
* consolidation commits the updated ledger as the body of an empty-tree commit, so
|
|
558
|
+
* grounding and retrieval surface it like any milestone; the prior ledger is read
|
|
559
|
+
* back from the last consolidation commit's body. One model call that MERGES new
|
|
560
|
+
* commits into the prior ledger, not a changelog.
|
|
561
|
+
*/
|
|
562
|
+
|
|
563
|
+
interface ConsolidateOptions {
|
|
564
|
+
/** Recent milestones to fold in. Default 30. */
|
|
565
|
+
max?: number;
|
|
566
|
+
/**
|
|
567
|
+
* Exclusive lower bound — fold only commits after this ref (e.g. the base
|
|
568
|
+
* branch). This scopes the fold to one line of work, exactly the set a squash
|
|
569
|
+
* merge collapses, so the consolidation can stand in as the squash body.
|
|
570
|
+
*/
|
|
571
|
+
since?: string;
|
|
572
|
+
/** The consolidated ledger so far, to update in place. */
|
|
573
|
+
prior?: string;
|
|
574
|
+
/** Engine for the (one) consolidation call. Default the run engine. */
|
|
575
|
+
engine?: EngineRef;
|
|
576
|
+
model?: string;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Fold the recent history into one decision-preserving consolidated ledger (a single
|
|
580
|
+
* model call). Returns the ledger text; the caller decides where it lives (see
|
|
581
|
+
* `consolidateJob`). Each commit is offered as its subject plus a body digest, so the
|
|
582
|
+
* exact decisions — not just the headings — reach the consolidation.
|
|
583
|
+
*/
|
|
584
|
+
declare function consolidate(ctx: JobContext, opts?: ConsolidateOptions): Promise<string>;
|
|
585
|
+
interface CompactOptions {
|
|
586
|
+
engine?: EngineRef;
|
|
587
|
+
model?: string;
|
|
588
|
+
/**
|
|
589
|
+
* The size budget for the working log in the commit body. A log already within
|
|
590
|
+
* it is kept VERBATIM (no model call); only a longer one is compacted, with this
|
|
591
|
+
* as the truncation fallback. Default 2000.
|
|
592
|
+
*/
|
|
593
|
+
maxChars?: number;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Compress a verbose working log (the ledger) into a tight summary for the commit
|
|
597
|
+
* body — one cheap model call. A short log (already within `maxChars`) is kept
|
|
598
|
+
* verbatim: compaction only earns its keep on a long log, and summarising a few
|
|
599
|
+
* lines just risks dropping the faithful "way" the commit body exists to preserve.
|
|
600
|
+
* Falls back to truncation when there is no usable reply or the call throws, so a
|
|
601
|
+
* commit never fails on compaction. '' in, '' out.
|
|
602
|
+
*/
|
|
603
|
+
declare function compactLedger(ctx: JobContext, text: string, opts?: CompactOptions): Promise<string>;
|
|
604
|
+
/**
|
|
605
|
+
* Compose the commit body — the handoff: everything the next agent needs if it lost all
|
|
606
|
+
* memory of this work. Two sources, in order: the agent's OWN handoff (captured verbatim by
|
|
607
|
+
* the handoff contract into `prompt.md`), else a structured handoff DISTILLED from the
|
|
608
|
+
* working log. The second path is the guarantee — loops owns the commit step, so a terse,
|
|
609
|
+
* instruction-skipping agent still leaves a rich, structured record rather than a bare
|
|
610
|
+
* "done". Returns '' only when there is nothing at all, so callers fall back to their floor.
|
|
611
|
+
*/
|
|
612
|
+
declare function composeCommitBody(ctx: JobContext, workspace: Workspace, opts?: CompactOptions): Promise<string>;
|
|
613
|
+
interface ConsolidateJobConfig extends ConsolidateOptions {
|
|
614
|
+
label?: string;
|
|
615
|
+
/** Commit subject; also how the prior ledger is found. Default `consolidate: ledger`. */
|
|
616
|
+
subject?: string;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Consolidate and commit the CONSOLIDATED LEDGER as a commit body. Reads the prior
|
|
620
|
+
* ledger from the last consolidation commit's body, folds in the recent history, and
|
|
621
|
+
* commits the updated ledger as the body of an empty-tree commit — so the coarse
|
|
622
|
+
* memory is durable and grounded-on like any milestone, never a tracked file.
|
|
623
|
+
*/
|
|
624
|
+
declare function consolidateJob(config?: ConsolidateJobConfig): Job;
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Conditions answer a yes/no question against the run context and the latest
|
|
628
|
+
* body outcome. They power a loop's `start`, `until`, and `stopOn` gates.
|
|
629
|
+
*
|
|
630
|
+
* Two flavours, same type:
|
|
631
|
+
* - deterministic (`predicate`, `bodyPassed`, `maxConfidence`)
|
|
632
|
+
* - agent-validated (`agentCheck`) — a small model returns a verdict +
|
|
633
|
+
* confidence, and the gate opens only above a threshold.
|
|
634
|
+
*
|
|
635
|
+
* `gateJob` lifts any Condition into a `Job`, so a reviewer can be expressed
|
|
636
|
+
* as a condition and still slot into `loop({ review })`.
|
|
637
|
+
*/
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Coerce any `ConditionInput` — a `Condition`, a bare predicate, or an array
|
|
641
|
+
* mixing both — into the single `Condition` primitive. This is what lets
|
|
642
|
+
* `until`/`start`/`stopOn` accept one or many items of either flavour.
|
|
643
|
+
*
|
|
644
|
+
* Arrays default to `all` (every item must hold); pass `'any'` for or-semantics.
|
|
645
|
+
*/
|
|
646
|
+
declare function toCondition(input: ConditionInput, combine?: 'all' | 'any'): Condition;
|
|
647
|
+
/** Deterministic predicate over context + last outcome. */
|
|
648
|
+
declare function predicate(fn: (ctx: JobContext, last: Outcome | undefined) => boolean | Promise<boolean>, reason?: string): Condition;
|
|
649
|
+
/** Met when the most recent body outcome passed. */
|
|
650
|
+
declare function bodyPassed(): Condition;
|
|
651
|
+
/** Met when the last outcome carries confidence at or above `threshold`. */
|
|
652
|
+
declare function minConfidence(threshold: number): Condition;
|
|
653
|
+
/**
|
|
654
|
+
* Deterministic gate that runs a shell command and is met on exit code 0. This
|
|
655
|
+
* is the honest convergence signal for coding loops: pair it with an `agentCheck`
|
|
656
|
+
* in an `until` array so the loop stops only when the tests ACTUALLY pass AND a
|
|
657
|
+
* judge agrees the work matches intent — never on a model's self-report alone.
|
|
658
|
+
* Runs in `cwd` (default: the process working dir), inherits the run's abort
|
|
659
|
+
* signal, and never throws (a spawn failure counts as "not met").
|
|
660
|
+
*/
|
|
661
|
+
declare function commandSucceeds(command: string, args?: string[], opts?: {
|
|
662
|
+
cwd?: string;
|
|
663
|
+
timeoutMs?: number;
|
|
664
|
+
}): Condition;
|
|
665
|
+
/**
|
|
666
|
+
* True when the branch's open PR has all required checks green — a synchronous
|
|
667
|
+
* "CI is green" gate over the `Forge`. Use it as `mergeJob`'s `when`, or anywhere a
|
|
668
|
+
* `Condition` is taken, for the blocking path; prefer `mergeJob({ auto: true })` to
|
|
669
|
+
* hand the same gate to GitHub non-blockingly. No PR / no branch → not met.
|
|
670
|
+
*/
|
|
671
|
+
declare function forgeChecks(): Condition;
|
|
672
|
+
declare const always: Condition;
|
|
673
|
+
declare const never: Condition;
|
|
674
|
+
declare function not(c: ConditionInput): Condition;
|
|
675
|
+
/** Met only when every input holds (short-circuits on the first failure). */
|
|
676
|
+
declare function all(...inputs: ConditionInput[]): Condition;
|
|
677
|
+
/** Met when any input holds (short-circuits on the first success). */
|
|
678
|
+
declare function any(...inputs: ConditionInput[]): Condition;
|
|
679
|
+
/**
|
|
680
|
+
* Met when at least `k` of the inputs hold. The honest hedge against a single
|
|
681
|
+
* agent judge's self-reported confidence: ask N independent judges and require a
|
|
682
|
+
* quorum (e.g. `quorum(2, j, j, j)`). All inputs run in parallel; a judge that
|
|
683
|
+
* throws counts as a "no" vote rather than sinking the whole gate. Each input
|
|
684
|
+
* may hit a model, so size N with cost in mind. Reported confidence is the mean
|
|
685
|
+
* of the holding inputs' confidences.
|
|
686
|
+
*/
|
|
687
|
+
declare function quorum(k: number, ...inputs: ConditionInput[]): Condition;
|
|
688
|
+
interface AgentCheckConfig {
|
|
689
|
+
/** The yes/no question the validator must answer. */
|
|
690
|
+
question: string;
|
|
691
|
+
/** Open the gate only at/above this confidence (0..1). Default 0.8. */
|
|
692
|
+
threshold?: number;
|
|
693
|
+
/** Small/cheap model recommended. A bare string — provider-agnostic. */
|
|
694
|
+
model?: string;
|
|
695
|
+
/**
|
|
696
|
+
* Give the judge a persona — an `AgentDef` whose resolved system (persona +
|
|
697
|
+
* skills) is prepended to the validator's scoring instructions, so a reviewer
|
|
698
|
+
* can be a named specialist (e.g. an adversarial reviewer) instead of an
|
|
699
|
+
* anonymous yes/no. The validator's output contract stays authoritative (it
|
|
700
|
+
* comes last); `model` falls back to the agent's `model`. Mirrors `agentJob`.
|
|
701
|
+
*/
|
|
702
|
+
agent?: AgentDef;
|
|
703
|
+
/** Engine for validation: a registered name, your own `Engine`, or default. */
|
|
704
|
+
engine?: EngineRef;
|
|
705
|
+
/**
|
|
706
|
+
* What the validator sees. By default: the last outcome's summary/data plus
|
|
707
|
+
* the shared state. Override to feed something bespoke — may be async, since a
|
|
708
|
+
* judge often gathers evidence (read the artifact, ground on the history, run a
|
|
709
|
+
* probe) before ruling. A blind judge cannot honestly confirm correctness, so
|
|
710
|
+
* give it the thing it is meant to be reviewing.
|
|
711
|
+
*/
|
|
712
|
+
context?: (ctx: JobContext, last: Outcome | undefined) => string | Promise<string>;
|
|
713
|
+
maxTokens?: number;
|
|
714
|
+
/**
|
|
715
|
+
* Score these named dimensions (0..1 each) instead of a single yes/no
|
|
716
|
+
* confidence. The gate opens when the GEOMETRIC MEAN of the scores is
|
|
717
|
+
* >= `threshold`, so one weak dimension drags the whole verdict down. A more
|
|
718
|
+
* honest judge than a lone self-reported number, e.g.
|
|
719
|
+
* `['intent match', 'evidence quality', 'outcome coherence']`.
|
|
720
|
+
*/
|
|
721
|
+
dimensions?: string[];
|
|
722
|
+
/**
|
|
723
|
+
* Parse a free-form review that closes with `<confidence>N%</confidence>`
|
|
724
|
+
* (N is 0-100) instead of forcing a JSON shape. The gate opens at/above
|
|
725
|
+
* `threshold`; the reviewer's prose before the tag becomes the gate's `reason`,
|
|
726
|
+
* so a failing review carries its findings to the next iteration (`lastReview`).
|
|
727
|
+
* More robust than scraping JSON, and the natural fit for a report-then-rate
|
|
728
|
+
* reviewer persona. Takes precedence over `dimensions`.
|
|
729
|
+
*/
|
|
730
|
+
confidenceTag?: boolean;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* A Condition decided by a (preferably small) model. With a single yes/no
|
|
734
|
+
* question the gate opens when the verdict is "yes" AND confidence >= threshold.
|
|
735
|
+
* With `dimensions`, the model scores each dimension 0..1 and the gate opens
|
|
736
|
+
* when their geometric mean >= threshold — a more honest judge than one number.
|
|
737
|
+
*/
|
|
738
|
+
declare function agentCheck(config: AgentCheckConfig): Condition;
|
|
739
|
+
/**
|
|
740
|
+
* Lift a Condition (or one-or-many `ConditionInput`) into a Job: `pass` when
|
|
741
|
+
* met, `fail` otherwise. This is how a reviewer becomes a drop-in `review` job
|
|
742
|
+
* (`gateJob('review', agentCheck(...))`).
|
|
743
|
+
*/
|
|
744
|
+
declare function gateJob(label: string, condition: ConditionInput): Job;
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* The engine registry — the drop-in mechanism. Built-ins are registered by
|
|
748
|
+
* name; anyone can `register(name, factory)` their own, or pass a ready-made
|
|
749
|
+
* `Engine` instance anywhere an `EngineRef` is accepted. Factories run lazily
|
|
750
|
+
* (on first `create`), so the Anthropic API engine never needs a key unless you
|
|
751
|
+
* actually select it.
|
|
752
|
+
*/
|
|
753
|
+
|
|
754
|
+
type EngineFactory = (opts: EngineOptions) => Engine;
|
|
755
|
+
declare class EngineRegistry {
|
|
756
|
+
private readonly opts;
|
|
757
|
+
private readonly factories;
|
|
758
|
+
private readonly cache;
|
|
759
|
+
constructor(opts?: EngineOptions);
|
|
760
|
+
/** Add or override an engine. The key is what you pass as an `EngineRef`. */
|
|
761
|
+
register(name: string, factory: EngineFactory): this;
|
|
762
|
+
has(name: string): boolean;
|
|
763
|
+
names(): string[];
|
|
764
|
+
/** Resolve a ref to an `Engine`: instance → as-is; name → built/cached. */
|
|
765
|
+
create(ref: EngineRef | undefined, fallback: EngineName): Engine;
|
|
766
|
+
private registerBuiltins;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* A scripted, offline engine — the reference "drop-in". It implements the same
|
|
771
|
+
* `Engine` interface as the real backends, so tests and examples run the exact
|
|
772
|
+
* same loop/dag/condition code paths with zero network. Writing one of these is
|
|
773
|
+
* all it takes to add a provider: implement `run`, register a name.
|
|
774
|
+
*/
|
|
775
|
+
|
|
776
|
+
type MockResponder = (req: AgentRequest) => string | {
|
|
777
|
+
text: string;
|
|
778
|
+
usage?: Usage;
|
|
779
|
+
model?: string;
|
|
780
|
+
};
|
|
781
|
+
declare class MockEngine implements Engine {
|
|
782
|
+
private readonly responder;
|
|
783
|
+
readonly name = "mock";
|
|
784
|
+
constructor(responder: MockResponder);
|
|
785
|
+
run(req: AgentRequest, onEvent: EngineEventSink, signal: AbortSignal): Promise<AgentResult>;
|
|
786
|
+
}
|
|
787
|
+
/** Convenience: always reply with a verdict JSON (handy for validator tests). */
|
|
788
|
+
declare function mockVerdict(verdict: 'yes' | 'no', confidence: number, reason?: string): MockEngine;
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* A scripted, offline environment — the reference Environment "drop-in", mirror
|
|
792
|
+
* of `MockEngine`. It simulates a deploy (hands back a URL + env vars, counts
|
|
793
|
+
* up/down) with zero network, so the lifecycle binding and gate integration run
|
|
794
|
+
* the exact same code paths in tests as a real sst/Vercel adapter would.
|
|
795
|
+
*/
|
|
796
|
+
|
|
797
|
+
interface MockEnvOptions {
|
|
798
|
+
/** The URL to hand back. A function derives it from the workspace (branch). */
|
|
799
|
+
url?: string | ((ws: Workspace) => string);
|
|
800
|
+
/** Extra env vars to inject alongside `BASE_URL`. */
|
|
801
|
+
env?: Record<string, string>;
|
|
802
|
+
onUp?: (ws: Workspace) => void;
|
|
803
|
+
onDown?: () => void;
|
|
804
|
+
}
|
|
805
|
+
declare class MockEnvironment implements Environment {
|
|
806
|
+
private readonly opts;
|
|
807
|
+
readonly name = "mock-env";
|
|
808
|
+
upCount: number;
|
|
809
|
+
downCount: number;
|
|
810
|
+
constructor(opts?: MockEnvOptions);
|
|
811
|
+
up(workspace: Workspace): Promise<EnvHandle>;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Stats collector. Subscribes to the event stream and accumulates everything
|
|
816
|
+
* the TUI footer and the exit summary need: per-loop iteration counts, review
|
|
817
|
+
* pass/fail tallies, token usage by model, errors, and wall-clock timing.
|
|
818
|
+
*/
|
|
819
|
+
|
|
820
|
+
interface LoopStat {
|
|
821
|
+
path: string;
|
|
822
|
+
iterations: number;
|
|
823
|
+
reviewsPassed: number;
|
|
824
|
+
reviewsFailed: number;
|
|
825
|
+
lastStatus?: Outcome['status'];
|
|
826
|
+
}
|
|
827
|
+
interface ModelUsage {
|
|
828
|
+
model: string;
|
|
829
|
+
calls: number;
|
|
830
|
+
inputTokens: number;
|
|
831
|
+
outputTokens: number;
|
|
832
|
+
}
|
|
833
|
+
interface ErrorEntry {
|
|
834
|
+
path: string;
|
|
835
|
+
code: string;
|
|
836
|
+
message: string;
|
|
837
|
+
ts: number;
|
|
838
|
+
}
|
|
839
|
+
interface StatsSnapshot {
|
|
840
|
+
startedAt: number;
|
|
841
|
+
elapsedMs: number;
|
|
842
|
+
loops: LoopStat[];
|
|
843
|
+
models: ModelUsage[];
|
|
844
|
+
totalInputTokens: number;
|
|
845
|
+
totalOutputTokens: number;
|
|
846
|
+
agentCalls: number;
|
|
847
|
+
errors: ErrorEntry[];
|
|
848
|
+
}
|
|
849
|
+
declare class Stats {
|
|
850
|
+
private readonly startedAt;
|
|
851
|
+
private readonly loops;
|
|
852
|
+
private readonly models;
|
|
853
|
+
private readonly errors;
|
|
854
|
+
record(event: LoopEvent): void;
|
|
855
|
+
snapshot(): StatsSnapshot;
|
|
856
|
+
private loopFor;
|
|
857
|
+
private modelFor;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* The runner assembles a `JobContext` and executes a root `Job` (a loop, a dag,
|
|
862
|
+
* or any job). It owns the engine registry, the abort controller, the shared
|
|
863
|
+
* state, and the stats collector. Reporters/TUI observe via `onEvent`.
|
|
864
|
+
*/
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Exit code for a `paused` run: EX_TEMPFAIL (sysexits.h). Distinct from `fail`
|
|
868
|
+
* (1) so a wrapper/cron can tell "paused, resumable" from "failed".
|
|
869
|
+
*/
|
|
870
|
+
declare const EXIT_PAUSED = 75;
|
|
871
|
+
interface RunOptions {
|
|
872
|
+
/** Default engine selected when a job/condition names none. Default agent-sdk. */
|
|
873
|
+
engine?: EngineName;
|
|
874
|
+
engineOptions?: EngineOptions;
|
|
875
|
+
/** Register custom engines (drop-in): name → factory or ready-made instance. */
|
|
876
|
+
engines?: Record<string, EngineFactory | Engine>;
|
|
877
|
+
/** External abort signal (the CLI wires SIGINT + keypress here). */
|
|
878
|
+
signal?: AbortSignal;
|
|
879
|
+
/** Root working directory the run operates in. Default: process.cwd(). */
|
|
880
|
+
cwd?: string;
|
|
881
|
+
/**
|
|
882
|
+
* Bring an environment up for the run (the root workspace) before the job, and
|
|
883
|
+
* tear it down after — so the gate can test the running thing. The adapter
|
|
884
|
+
* (sst, Vercel, …) is yours; loops owns only the seam. Per-team environments
|
|
885
|
+
* at the worktree boundary are a separate, later binding.
|
|
886
|
+
*/
|
|
887
|
+
environment?: Environment;
|
|
888
|
+
/**
|
|
889
|
+
* The PR host for `pushJob`/`pullRequestJob`/`mergeJob`. Default: `GhForge`
|
|
890
|
+
* (the `gh` CLI) when a job needs one. Pass a `MockForge` to run offline.
|
|
891
|
+
*/
|
|
892
|
+
forge?: Forge;
|
|
893
|
+
onEvent?: (event: LoopEvent) => void;
|
|
894
|
+
/** Seed the shared, mutable run state. */
|
|
895
|
+
state?: Record<string, unknown>;
|
|
896
|
+
/**
|
|
897
|
+
* Cap total tokens (input + output) for the run. A bare number is the limit;
|
|
898
|
+
* pass `{ limit, headroom?, soft? }` for headroom or warn-don't-refuse mode.
|
|
899
|
+
* Engine call sites refuse to spend past it (see `Budget`).
|
|
900
|
+
*/
|
|
901
|
+
budget?: number | BudgetConfig;
|
|
902
|
+
/** Append every structured event as JSONL here — a readable run record. */
|
|
903
|
+
recordTo?: string;
|
|
904
|
+
/** Snapshot the shared run state here at each loop/dag/job boundary. */
|
|
905
|
+
checkpoint?: string;
|
|
906
|
+
/** Restore shared run state written by a prior `checkpoint` before starting. */
|
|
907
|
+
resumeFrom?: string;
|
|
908
|
+
/**
|
|
909
|
+
* How a loop reacts to a rate limit / quota / token budget. Default `auto`:
|
|
910
|
+
* wait out a known reset within `maxWaitMs`, else checkpoint and exit with a
|
|
911
|
+
* resume command (the `paused` status, exit code 75). `wait` waits any known
|
|
912
|
+
* reset with no ceiling; `exit-resume` never waits; `fail` is the old fatal
|
|
913
|
+
* behaviour.
|
|
914
|
+
*/
|
|
915
|
+
onLimit?: LimitPolicy;
|
|
916
|
+
/** Ceiling on a single interruptible limit-wait, in ms. Default 300000. */
|
|
917
|
+
maxWaitMs?: number;
|
|
918
|
+
/**
|
|
919
|
+
* Ready-to-paste command to resume a paused run, surfaced to reporters and the
|
|
920
|
+
* `limit:pause` event. The CLI reconstructs this from the invocation.
|
|
921
|
+
*/
|
|
922
|
+
resumeCommand?: string;
|
|
923
|
+
}
|
|
924
|
+
interface RunResult {
|
|
925
|
+
outcome: Outcome;
|
|
926
|
+
stats: StatsSnapshot;
|
|
927
|
+
/** Final token accounting, when a budget was set. */
|
|
928
|
+
budget?: {
|
|
929
|
+
limit: number;
|
|
930
|
+
spent: number;
|
|
931
|
+
remaining: number;
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
declare function run(job: Job, options?: RunOptions): Promise<RunResult>;
|
|
935
|
+
/** Process exit code mapped from a terminal outcome. */
|
|
936
|
+
declare function exitCodeFor(outcome: Outcome): number;
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Public API. A loop-definition file imports from here and `export default`s a
|
|
940
|
+
* `Job` (usually a `loop(...)` or `dag(...)`). The CLI runs that default export.
|
|
941
|
+
*
|
|
942
|
+
* import { loop, agentJob, agentCheck, defineJob } from 'loops';
|
|
943
|
+
* export default defineJob(loop({ ... }));
|
|
944
|
+
*/
|
|
945
|
+
|
|
946
|
+
/** Identity helper that pins the type of a default export to `Job`. */
|
|
947
|
+
declare function defineJob(job: Job): Job;
|
|
948
|
+
|
|
949
|
+
export { type AgentCheckConfig, AgentDef, AgentRequest, AgentResult, BudgetConfig, type CommitInput, type CommitRecord, type CompactOptions, Condition, ConditionInput, type ConsolidateJobConfig, type ConsolidateOptions, DagConfig, EXIT_PAUSED, Engine, type EngineFactory, EngineName, EngineOptions, EngineRef, EngineRegistry, EnvHandle, Environment, Forge, type GroundOptions, type IsolatedOptions, Job, JobContext, JobMeta, type LedgerEntry, LimitPolicy, type LogQuery, LoopConfig, LoopEvent, type MergeJobConfig, type MergeResult, type MergeSynthesisConfig, type MergeSynthesisResult, MockEngine, type MockEnvOptions, MockEnvironment, type MockResponder, Outcome, type PromptNote, type PullRequestJobConfig, type PushJobConfig, type PushOptions, type PushResult, type RetrieveOptions, type RunOptions, type RunResult, Stats, type StatsSnapshot, type TournamentConfig, Usage, Workspace, type WorktreeHandle, addWorktree, agentCheck, all, always, any, appendLedger, appendPrompt, bodyPassed, commandSucceeds, commit, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, dag, defineJob, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, forgeChecks, gateJob, groundingText, hasStagedChanges, headSha, isDirty, isRepo, isolated, jobMeta, ledgerPath, log, loop, mergeAbort, mergeBranch, mergeJob, mergeNoCommit, mergeSynthesis, minConfidence, mockVerdict, never, not, parallel, predicate, promptPath, pullRequestJob, push, pushJob, quorum, readLedger, readPrompt, removeWorktree, renderPlan, resetLedger, resetPrompt, retrieveLedger, run, sequence, stageAll, toCondition, tournament };
|