@tonoid/agent-loop 1.0.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +378 -0
  3. package/briefs/default/build.md +35 -0
  4. package/briefs/default/core.md +52 -0
  5. package/briefs/default/journal.optional.md +7 -0
  6. package/briefs/default/review.md +65 -0
  7. package/briefs/default/routine.md +18 -0
  8. package/briefs/default/screenshots.optional.md +10 -0
  9. package/briefs/default/subagents.optional.md +7 -0
  10. package/docs/cutover.md +121 -0
  11. package/package.json +46 -0
  12. package/src/adapters/gh.ts +77 -0
  13. package/src/adapters/git.ts +82 -0
  14. package/src/adapters/herdr.ts +121 -0
  15. package/src/adapters/run.ts +64 -0
  16. package/src/adopt.ts +47 -0
  17. package/src/brief.ts +124 -0
  18. package/src/check.ts +126 -0
  19. package/src/cli-pause.ts +16 -0
  20. package/src/cli.ts +290 -0
  21. package/src/config.ts +211 -0
  22. package/src/ctx.ts +64 -0
  23. package/src/discover.ts +344 -0
  24. package/src/effects/monitor.ts +148 -0
  25. package/src/effects/spawn.ts +246 -0
  26. package/src/effects/sweep.ts +27 -0
  27. package/src/engine/item.ts +32 -0
  28. package/src/engine/monitor.ts +117 -0
  29. package/src/engine/naming.ts +45 -0
  30. package/src/engine/spawn.ts +101 -0
  31. package/src/engine/sweep.ts +91 -0
  32. package/src/engine/tick.ts +25 -0
  33. package/src/filing.ts +56 -0
  34. package/src/globalstate.ts +228 -0
  35. package/src/journal.ts +13 -0
  36. package/src/kinds/builder.ts +128 -0
  37. package/src/kinds/index.ts +13 -0
  38. package/src/kinds/reviewer.ts +220 -0
  39. package/src/kinds/routine.ts +174 -0
  40. package/src/kinds/shared.ts +49 -0
  41. package/src/kinds/validate.ts +187 -0
  42. package/src/lock.ts +63 -0
  43. package/src/paths.ts +21 -0
  44. package/src/render.ts +42 -0
  45. package/src/router/budget.ts +81 -0
  46. package/src/router/providers/claude.ts +177 -0
  47. package/src/router/providers/codex.ts +120 -0
  48. package/src/router/providers/grok.ts +10 -0
  49. package/src/router/rate.ts +62 -0
  50. package/src/router/route.ts +179 -0
  51. package/src/router/window.ts +39 -0
  52. package/src/runtime/worker.ts +75 -0
  53. package/src/state.ts +128 -0
  54. package/src/status.ts +41 -0
  55. package/src/types.ts +225 -0
package/src/types.ts ADDED
@@ -0,0 +1,225 @@
1
+ export type Provider = "claude" | "codex" | "grok"
2
+
3
+ export interface AccountConfig {
4
+ id: string
5
+ provider: Provider
6
+ configDir: string
7
+ reserve: number
8
+ // Held back per weekday the human still has before the window resets, on top
9
+ // of the flat reserve, which stays the floor. A weekly quota resetting on
10
+ // Sunday is one working day away, not three.
11
+ reservePerWeekday?: number
12
+ // True when loop workers are the only thing spending this account, which is
13
+ // the one condition under which its usage deltas measure a worker. Nothing
14
+ // else implies it: reserve is about protecting quota, not about who spends it.
15
+ soleConsumer?: boolean
16
+ // What an hour of a Saturday or Sunday is worth against an hour of a weekday,
17
+ // so a weekend keeps a small assignment rather than none. 0.25 by default.
18
+ weekendWeight?: number
19
+ maxConcurrent?: number
20
+ allowWhenUnreadable?: boolean
21
+ // The herdr agent kind this account's workers start. Spelled out rather than
22
+ // `kind`, which names a job's behavior in job.yml.
23
+ agentKind?: string
24
+ startArgs?: string[]
25
+ // The model this account's workers run. Model-scoped usage windows that name
26
+ // a different model are skipped. Unset keeps every scoped window, which is
27
+ // the conservative direction: an extra constraint can only lower concurrency.
28
+ model?: string
29
+ // Overrides the built-in OAuth client id used to refresh this account.
30
+ oauthClientId?: string
31
+ // The environment variable this provider reads its config directory from.
32
+ // Defaults per provider; set it explicitly for a provider whose variable
33
+ // the loop has not verified.
34
+ configEnv?: string
35
+ }
36
+
37
+ export interface NamingConfig {
38
+ labels: { claim: string; failed: string; park: string; priority: string[] }
39
+ mergeMethod: "merge" | "squash"
40
+ }
41
+
42
+ export interface WorkspaceConfig {
43
+ // The state directory key. Explicit rather than derived from the folder,
44
+ // which is called agent-loop in every project.
45
+ name: string
46
+ // The folder holding workspace.yml. Every relative path resolves from here.
47
+ dir: string
48
+ herdrWorkspace: string
49
+ worktreeBase: string
50
+ repos: Record<string, string>
51
+ naming: NamingConfig
52
+ // ~/.agent-loop/<name>/journal.md. Machine-local, so it is derived rather
53
+ // than configured: a path to it in a versioned file would not travel.
54
+ journalPath: string
55
+ jobs: Job[]
56
+ }
57
+
58
+ export interface Config {
59
+ accounts: AccountConfig[]
60
+ // Absolute paths, one per workspace folder. The definitions live at those
61
+ // paths and are read fresh on every tick; see src/discover.ts.
62
+ workspaces: string[]
63
+ maxConcurrentPerAccount: number
64
+ minFreeMb: number
65
+ usageMax: number
66
+ releaseBefore: number
67
+ maxSpawnsPerDay: number
68
+ blockedTimeoutMin: number
69
+ // Percentage points per minute per worker, used for a provider/window pair
70
+ // with no measured EWMA yet. Must be > 0.
71
+ workerRateSeed: number
72
+ }
73
+
74
+ export type ItemState = "OPEN" | "CLOSED" | "MERGED"
75
+
76
+ export interface WorkItem {
77
+ id: string
78
+ number: number
79
+ title: string
80
+ state: ItemState
81
+ labels: string[]
82
+ headRef?: string
83
+ url?: string
84
+ // ISO 8601, as the forge reports it. The filing audit needs a time bound and
85
+ // nothing else in the engine reads it.
86
+ createdAt?: string
87
+ }
88
+
89
+ export type AgentStatus = "working" | "blocked" | "idle" | "missing"
90
+
91
+ export interface AgentView {
92
+ cwd: string
93
+ status: AgentStatus
94
+ paneId: string
95
+ }
96
+
97
+ export interface Marks {
98
+ has(job: string, key: string, mark: string): boolean
99
+ age(job: string, key: string, mark: string): number | null
100
+ set(job: string, key: string, mark: string): void
101
+ clear(job: string, key: string, mark: string): void
102
+ gc(olderThanDays: number): number
103
+ }
104
+
105
+ export interface Ctx {
106
+ workspace: WorkspaceConfig
107
+ // Every workspace this process is ticking, so account-scoped facts are
108
+ // counted across the box rather than per workspace (spec 7).
109
+ workspaces: WorkspaceConfig[]
110
+ config: Config
111
+ now: Date
112
+ // Whether this tick may mutate anything at all: the outside world, and
113
+ // agent-loop's own marks. The same flag arms the refusal in adapters/run.ts,
114
+ // so a false here and a mutation attempted anyway is a thrown error, not a
115
+ // silent write. The global database is the exception and records usage
116
+ // samples either way: measuring a quota is not a mutation, and it is what
117
+ // warms the router's rate estimate during a shadow week.
118
+ live: boolean
119
+ // Injected so the worker start dance's retries do not make the suite wait.
120
+ sleep(ms: number): Promise<void>
121
+ lock: import("./lock").LockImpl
122
+ gh: import("./adapters/gh").Gh
123
+ git(repo: string): import("./adapters/git").Git
124
+ herdr: import("./adapters/herdr").HerdrRead
125
+ marks: Marks
126
+ global: import("./globalstate").GlobalStore
127
+ usage(a: AccountConfig): Promise<AccountUsage>
128
+ memAvailableMb(): Promise<number>
129
+ log(d: Decision): void
130
+ cache<T>(key: string, fn: () => Promise<T>): Promise<T>
131
+ }
132
+
133
+ // Backpressure, spec 5.2. Every job that creates work for another declares it,
134
+ // and the engine derives the budget from the consumer's depth rather than from
135
+ // a constant, so it closes on its own when the pipeline backs up.
136
+ export interface FilingConfig {
137
+ // The consumer job whose queue this one feeds.
138
+ queue: string
139
+ maxOpen: number
140
+ perRound: number
141
+ dedupeBy: string
142
+ }
143
+
144
+ export interface Job {
145
+ name: string
146
+ // The job folder. Brief paths resolve from here.
147
+ dir: string
148
+ // Spawn walk order, ties broken by name. Reviewers before builders, so a
149
+ // merge in this tick relieves the builder's review-debt throttle in the
150
+ // same tick.
151
+ order?: number
152
+ workload: string
153
+ slots?: number
154
+ repo?: string
155
+ // The model this job's workers run, as the agent's own alias or full id.
156
+ // A job-level knob rather than an account one because the router picks the
157
+ // account by headroom: the same job must run the same model wherever it lands.
158
+ model?: string
159
+ sweepIgnoresWorking?: boolean
160
+ deleteRemote?: boolean
161
+ // Selectors: each matches an account by id or by provider.
162
+ requires?: string[]
163
+ prefer?: string[]
164
+ // Demote the account recorded as "built-by:" in this item's PR body.
165
+ distinctFrom?: boolean
166
+ filing?: FilingConfig
167
+ admit?(ctx: Ctx): Promise<string | null>
168
+ discover(ctx: Ctx): Promise<WorkItem[]>
169
+ discoverClaimed(ctx: Ctx): Promise<WorkItem[]>
170
+ key(ctx: Ctx, item: WorkItem): Promise<string>
171
+ attempt?(ctx: Ctx, item: WorkItem): Promise<number>
172
+ guard?(ctx: Ctx, item: WorkItem): Promise<boolean>
173
+ done(ctx: Ctx, item: WorkItem): Promise<boolean>
174
+ sweepOk?(ctx: Ctx, rawKey: string): Promise<boolean>
175
+ // Files copied into a fresh worktree before the worker starts; missing
176
+ // files are ignored and mode is preserved.
177
+ copyIntoWorktree?: string[]
178
+ base?(ctx: Ctx, item: WorkItem): Promise<string>
179
+ prepare?(ctx: Ctx, worktree: string): Promise<void>
180
+ // The brief is the product: what the worker is told to do. Plan 4 ships the
181
+ // default briefs; this plan only delivers whatever the job returns.
182
+ brief(ctx: Ctx, item: WorkItem): Promise<string>
183
+ nudge?(ctx: Ctx, item: WorkItem): Promise<string>
184
+ escalate?(ctx: Ctx, item: WorkItem): Promise<string>
185
+ onFail?(ctx: Ctx, item: WorkItem, transcriptTail: string): Promise<void>
186
+ }
187
+
188
+ export type Decision =
189
+ | { pass: "gc"; removed: number }
190
+ | { pass: "sweep"; job: string; worktree: string; branch: string; action: "clean" | "hold"; reason: string }
191
+ | { pass: "monitor"; job: string; key: string; action: MonitorAction; reason: string }
192
+ | { pass: "spawn"; job: string; key: string; action: "spawn" | "skip"; account?: string; reason: string }
193
+ // The workspace this tick pass covered, or "total" for the whole process:
194
+ // one cron line covers every workspace on the box, and the cadence rule in
195
+ // spec 4.2 has to be read against the sum, not one workspace's share.
196
+ | { pass: "tick"; workspace: string; ms: number }
197
+ | { pass: "error"; job: string; where: "sweep" | "monitor" | "spawn"; reason: string }
198
+ | { pass: "error"; where: "workspace"; workspace: string; reason: string }
199
+ // The filing audit, at sweep time. Advisory in the brief, audited here.
200
+ | { pass: "audit"; job: string; key: string; filed: number; budget: number }
201
+ // Anything a job wants an operator to see that is not a decision about an
202
+ // item: an identity that could not be resolved, a constraint ignored.
203
+ | { pass: "warn"; job: string; reason: string }
204
+
205
+ export type MonitorAction =
206
+ | "done" | "external" | "busy" | "blocked" | "escalate"
207
+ | "restart" | "nudge" | "fail" | "hold"
208
+
209
+ export interface Window {
210
+ kind: string // 'session' | 'weekly_all' | 'w300' | ...
211
+ group: string
212
+ percent: number
213
+ resetsAt: Date
214
+ windowMinutes: number
215
+ scope?: { model?: string }
216
+ observedAt: Date
217
+ }
218
+
219
+ // `exhausted` is a 429: strictly more information than "unknown", and the one
220
+ // unreadable state that allowWhenUnreadable must not resurrect.
221
+ export type AccountUsage =
222
+ | { readable: true; windows: Window[] }
223
+ | { readable: false; reason: string; exhausted?: boolean }
224
+
225
+ export type UsageReader = (a: AccountConfig, now: Date) => Promise<AccountUsage>