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