@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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +486 -0
  3. package/bin/loops.mjs +16 -0
  4. package/dist/App-3YQS6DXA.js +461 -0
  5. package/dist/App-3YQS6DXA.js.map +1 -0
  6. package/dist/agent-sdk-RF5VJZAT.js +95 -0
  7. package/dist/agent-sdk-RF5VJZAT.js.map +1 -0
  8. package/dist/anthropic-api-XJY6Y4T2.js +131 -0
  9. package/dist/anthropic-api-XJY6Y4T2.js.map +1 -0
  10. package/dist/api.d.ts +949 -0
  11. package/dist/api.js +898 -0
  12. package/dist/api.js.map +1 -0
  13. package/dist/chunk-33YIGWNU.js +63 -0
  14. package/dist/chunk-33YIGWNU.js.map +1 -0
  15. package/dist/chunk-3BPU34DE.js +2163 -0
  16. package/dist/chunk-3BPU34DE.js.map +1 -0
  17. package/dist/chunk-CXEPZHSR.js +86 -0
  18. package/dist/chunk-CXEPZHSR.js.map +1 -0
  19. package/dist/chunk-I3STY7U6.js +61 -0
  20. package/dist/chunk-I3STY7U6.js.map +1 -0
  21. package/dist/chunk-JFTXJ7I2.js +18 -0
  22. package/dist/chunk-JFTXJ7I2.js.map +1 -0
  23. package/dist/chunk-XC46B4FD.js +9 -0
  24. package/dist/chunk-XC46B4FD.js.map +1 -0
  25. package/dist/chunk-Y2SD7GBL.js +30 -0
  26. package/dist/chunk-Y2SD7GBL.js.map +1 -0
  27. package/dist/claude-cli-U7WEVAOL.js +124 -0
  28. package/dist/claude-cli-U7WEVAOL.js.map +1 -0
  29. package/dist/codex-6I5UZ2HM.js +60 -0
  30. package/dist/codex-6I5UZ2HM.js.map +1 -0
  31. package/dist/env/command.d.ts +53 -0
  32. package/dist/env/command.js +3 -0
  33. package/dist/env/command.js.map +1 -0
  34. package/dist/env/docker.d.ts +38 -0
  35. package/dist/env/docker.js +33 -0
  36. package/dist/env/docker.js.map +1 -0
  37. package/dist/env/sst.d.ts +39 -0
  38. package/dist/env/sst.js +20 -0
  39. package/dist/env/sst.js.map +1 -0
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.js +620 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/types-B4wGVpqo.d.ts +898 -0
  44. package/package.json +100 -0
  45. 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 };