@aivi/plugin 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -2
- package/dist/api.d.ts +10 -0
- package/dist/api.js +147 -0
- package/dist/channel.d.ts +184 -0
- package/dist/channel.js +13 -0
- package/dist/cli.d.ts +71 -0
- package/dist/cli.js +26 -0
- package/dist/dispatcher.d.ts +67 -0
- package/dist/dispatcher.js +13 -0
- package/dist/forge.d.ts +243 -0
- package/dist/forge.js +30 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/module.d.ts +306 -0
- package/dist/module.js +28 -0
- package/dist/plugin.d.ts +38 -0
- package/dist/plugin.js +13 -0
- package/dist/run.d.ts +144 -0
- package/dist/run.js +18 -0
- package/dist/setup-project.d.ts +128 -0
- package/dist/setup-project.js +48 -0
- package/dist/setup.d.ts +62 -0
- package/dist/setup.js +24 -0
- package/dist/tracker.d.ts +473 -0
- package/dist/tracker.js +38 -0
- package/dist/voice.d.ts +148 -0
- package/dist/voice.js +23 -0
- package/package.json +41 -4
package/dist/forge.d.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **forge** contract: the seam between aivi and a repository host. A
|
|
3
|
+
* forge owns the *remote*: the repository a project's checkout came from, the
|
|
4
|
+
* pull requests over its branches, the review conversation on them, and the
|
|
5
|
+
* credential that lets aivi move bytes as its own app.
|
|
6
|
+
*
|
|
7
|
+
* The line between a forge and aivi is **remote, not git** (ruled
|
|
8
|
+
* 2026-09-29): any git operation that reaches `origin` authenticates to it,
|
|
9
|
+
* so it belongs to the forge — clone, fetch, the sync of a clean checkout,
|
|
10
|
+
* push. What only touches the local store is aivi's own machinery (the
|
|
11
|
+
* orchestrator creates and reaps worktrees, the worker commits in one) and
|
|
12
|
+
* never appears here. A forge uses its API for what git cannot see: pull
|
|
13
|
+
* requests and reviews.
|
|
14
|
+
*
|
|
15
|
+
* Types only — the machinery consuming them ships in the host. The exact
|
|
16
|
+
* member set is **provisional by design**
|
|
17
|
+
* ([forge-github.md](../../docs/plans/forge-github.md)): the orchestrator's
|
|
18
|
+
* wiring is what proves it, and more forges will move the details.
|
|
19
|
+
*
|
|
20
|
+
* A project **having no forge is not a failure** (ruled 2026-09-29): a ticket
|
|
21
|
+
* can be "research X, write it up" and complete without aivi ever asking a
|
|
22
|
+
* repository host anything. Nothing here may assume a worker touches a remote.
|
|
23
|
+
*
|
|
24
|
+
* Cloning is a forge's operation too, but it is not on this interface: it
|
|
25
|
+
* happens once, at project setup, through the plugin's `./setupProject`
|
|
26
|
+
* contributor ([setup-project.ts](setup-project.ts)), which is where a person
|
|
27
|
+
* is asked which repository they mean. By the time a `Forge` is speaking to
|
|
28
|
+
* aivi the checkout already exists.
|
|
29
|
+
*/
|
|
30
|
+
/** What a forge needs to know about a project to recognise it: the directory
|
|
31
|
+
* aivi discovered the checkout in. Reading `origin` from it is a local git
|
|
32
|
+
* read — reaching `origin` is the forge's own business, below. */
|
|
33
|
+
export interface ForgeProject {
|
|
34
|
+
id: string;
|
|
35
|
+
directory: string;
|
|
36
|
+
}
|
|
37
|
+
/** One repository of this forge, in the forge's own terms. Opaque to aivi:
|
|
38
|
+
* it is handed back to every other call and never inspected. */
|
|
39
|
+
export interface RepoRef {
|
|
40
|
+
/** The forge's name for it (`owner/repo`); what logs and errors say. */
|
|
41
|
+
id: string;
|
|
42
|
+
/** The remote URL aivi would use to reach it — the forge's own to
|
|
43
|
+
* authenticate; nothing outside the forge puts a credential in it. */
|
|
44
|
+
remote: string;
|
|
45
|
+
}
|
|
46
|
+
/** A pull request over a branch: the fact a tracker may also report from its
|
|
47
|
+
* own knowledge, and the forge answers when it cannot. */
|
|
48
|
+
export interface PrFacts {
|
|
49
|
+
/** The forge's id for the pull request; what `resolveThread` and friends
|
|
50
|
+
* are called with. */
|
|
51
|
+
id: string;
|
|
52
|
+
url: string;
|
|
53
|
+
title: string;
|
|
54
|
+
/** The platform's own state word (`open`, `merged`, `closed`). */
|
|
55
|
+
state: string;
|
|
56
|
+
/** The branch it stands for — the one thing aivi correlates on. */
|
|
57
|
+
branch: string;
|
|
58
|
+
/** Whether the base branch could take this merge today, so "main moved,
|
|
59
|
+
* you conflict" is in the worker's first prompt instead of a surprise at
|
|
60
|
+
* push time. `unknown` is the honest answer where the platform has not
|
|
61
|
+
* computed it (a REST list read never says); a forge that cannot say
|
|
62
|
+
* leaves it absent rather than guessing clean. */
|
|
63
|
+
mergeable?: 'clean' | 'dirty' | 'unknown';
|
|
64
|
+
}
|
|
65
|
+
/** Who wrote a review comment. Aivi's own posts are *signed* — visibly, in
|
|
66
|
+
* text a human reads — and the forge parses that signature back into
|
|
67
|
+
* `worker`, so a wake can tell its own past comments from a human's. */
|
|
68
|
+
export interface ReviewAuthor {
|
|
69
|
+
/** The platform's login (`octocat`, `aivi-agent[bot]`). */
|
|
70
|
+
login: string;
|
|
71
|
+
/** The worker role aivi posted as, read back out of the comment's own
|
|
72
|
+
* signature; absent for a human and for anything aivi did not write. */
|
|
73
|
+
worker?: string;
|
|
74
|
+
/** True when the platform says the author is an app or bot. */
|
|
75
|
+
bot: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** One review thread. Only **open threads** are ever handed to a worker
|
|
78
|
+
* (ruled 2026-09-29): there is no `since` cursor, a wake takes what is open
|
|
79
|
+
* and the worker must bring all of it to zero — by agree (commit, then the
|
|
80
|
+
* forge pushes on the orchestrator's word) or by decline (comment and
|
|
81
|
+
* resolve). */
|
|
82
|
+
export interface ReviewThread {
|
|
83
|
+
/** The forge's id for the thread; what `resolveThread` is called with. */
|
|
84
|
+
id: string;
|
|
85
|
+
/** The file the thread speaks to, when it is anchored to one. */
|
|
86
|
+
path?: string;
|
|
87
|
+
/** The platform's own state word (`open`, `resolved`, `outdated`). */
|
|
88
|
+
state: string;
|
|
89
|
+
/** The thread's opening body — what the worker is being asked. */
|
|
90
|
+
question: string;
|
|
91
|
+
/** Everything said since, oldest first, each with its author. */
|
|
92
|
+
replies: {
|
|
93
|
+
author: ReviewAuthor;
|
|
94
|
+
body: string;
|
|
95
|
+
}[];
|
|
96
|
+
}
|
|
97
|
+
/** What a worker wake gathers about a pull request: the state of the review
|
|
98
|
+
* in neutral terms, and the open threads it must answer. */
|
|
99
|
+
export interface ReviewFacts {
|
|
100
|
+
pr: PrFacts;
|
|
101
|
+
/** Approvals in the platform's own words (`APPROVED`, `CHANGES_REQUESTED`). */
|
|
102
|
+
reviews: {
|
|
103
|
+
author: ReviewAuthor;
|
|
104
|
+
state: string;
|
|
105
|
+
}[];
|
|
106
|
+
/** The threads a worker still owes an answer on. Empty means a clean
|
|
107
|
+
* review: nothing to resolve, nothing to reply to. */
|
|
108
|
+
threads: ReviewThread[];
|
|
109
|
+
/** The pull request's plain conversation comments — not review threads,
|
|
110
|
+
* and the platform gives them **no resolved state**, so they are
|
|
111
|
+
* context, never gate items (ruled 2026-10-02). Oldest first, capped
|
|
112
|
+
* like the thread reads; the agent file's guidance says to answer them. */
|
|
113
|
+
comments: {
|
|
114
|
+
author: ReviewAuthor;
|
|
115
|
+
body: string;
|
|
116
|
+
createdAt: string;
|
|
117
|
+
}[];
|
|
118
|
+
}
|
|
119
|
+
/** What a forge answers and does. Everything here is either a **fact aivi
|
|
120
|
+
* cannot compute from a clone** or a **remote operation only the forge may
|
|
121
|
+
* authenticate for**. */
|
|
122
|
+
export interface Forge {
|
|
123
|
+
/** The forge's name for the repository a project's `origin` names, or
|
|
124
|
+
* undefined when the remote is not this forge's — another forge's turn,
|
|
125
|
+
* or no forge at all. */
|
|
126
|
+
repoFor(project: ForgeProject): Promise<RepoRef | undefined>;
|
|
127
|
+
/** The pull request over a branch, if one exists. A tracker may answer
|
|
128
|
+
* this from its own knowledge; the forge is the fallback that can say. */
|
|
129
|
+
prForBranch(repo: RepoRef, branch: string): Promise<PrFacts | undefined>;
|
|
130
|
+
/** The review conversation: the open threads and the state of the
|
|
131
|
+
* approvals, gathered for a worker about to wake. */
|
|
132
|
+
reviewFeedback(repo: RepoRef, pr: PrFacts): Promise<ReviewFacts>;
|
|
133
|
+
/** Answer one open thread as aivi and resolve it. The orchestrator calls
|
|
134
|
+
* this with the message from a validated resolution report; the worker
|
|
135
|
+
* never holds forge credentials to write review traffic, and the comment
|
|
136
|
+
* says which worker role it came from — visibly. */
|
|
137
|
+
resolveThread(repo: RepoRef, pr: PrFacts, threadId: string, reply: {
|
|
138
|
+
author: string;
|
|
139
|
+
text: string;
|
|
140
|
+
}): Promise<void>;
|
|
141
|
+
/** Post a plain conversation comment on the pull request — the non-thread
|
|
142
|
+
* channel a worker uses to say something grounded when there is no
|
|
143
|
+
* thread to answer. Signed like every aivi post. */
|
|
144
|
+
commentPr(repo: RepoRef, pr: PrFacts, comment: {
|
|
145
|
+
author: string;
|
|
146
|
+
text: string;
|
|
147
|
+
}): Promise<void>;
|
|
148
|
+
/** Say *on the pull request itself* what aivi's review found: a review
|
|
149
|
+
* with inline findings, which the platform shows as review threads — the
|
|
150
|
+
* only way agent feedback becomes gate-owed on the next round. APPROVE
|
|
151
|
+
* is not offered: the app authored the pull request and GitHub refuses an
|
|
152
|
+
* author's own approval, so the approve button stays human forever
|
|
153
|
+
* (ruled 2026-10-02). */
|
|
154
|
+
submitReview(repo: RepoRef, pr: PrFacts, review: {
|
|
155
|
+
author: string;
|
|
156
|
+
body: string;
|
|
157
|
+
state: 'COMMENT' | 'REQUEST_CHANGES';
|
|
158
|
+
comments?: {
|
|
159
|
+
path: string;
|
|
160
|
+
line?: number;
|
|
161
|
+
body: string;
|
|
162
|
+
}[];
|
|
163
|
+
}): Promise<void>;
|
|
164
|
+
/** Bring a project's clean checkout up to date with `origin`: fetch, then
|
|
165
|
+
* fast-forward the checked-out branch. Anything needing a decision (local
|
|
166
|
+
* changes, a detached head, diverged history) is reported, never forced —
|
|
167
|
+
* the checkout aivi indexes is not a working directory. */
|
|
168
|
+
syncSource(repo: RepoRef, directory: string): Promise<ForgeSync>;
|
|
169
|
+
/** Carry one branch's remote tip into the checkout's own remote refs
|
|
170
|
+
* (`refs/remotes/origin/<branch>`) so the orchestrator can start a
|
|
171
|
+
* worktree from it **without ever touching the remote itself** — every
|
|
172
|
+
* external boundary is crossed by using the forge (ruled 2026-10-02),
|
|
173
|
+
* and what aivi does with the ref afterwards is local git. A remote
|
|
174
|
+
* without that branch — a ticket branch never pushed — says so, and
|
|
175
|
+
* that is an answer, not a failure: it returns quietly. A transfer that
|
|
176
|
+
* failed throws, because the caller cannot tell a stale tip from an
|
|
177
|
+
* absent one. */
|
|
178
|
+
fetchBranch(repo: RepoRef, directory: string, branch: string): Promise<void>;
|
|
179
|
+
/** Move aivi's own commits to `origin`, as the app and never as the
|
|
180
|
+
* person at the keyboard. The orchestrator's smart push decides *how*:
|
|
181
|
+
* plain when the remote fast-forwards (or the branch is new), and with
|
|
182
|
+
* `lease` — the commit it just fetched as the branch's tip — when the
|
|
183
|
+
* divergence is only the worker's own rewrite, so the force replaces
|
|
184
|
+
* exactly what the worker rebased over and nothing a person added
|
|
185
|
+
* since. How the forge authenticates — mint once, cache to its TTL,
|
|
186
|
+
* re-mint on a 401 — is the forge's own choice and never written into
|
|
187
|
+
* the worktree's `.git/config`. A transfer the remote refuses throws
|
|
188
|
+
* with git's words; a lease that no longer matches is exactly that. */
|
|
189
|
+
push(repo: RepoRef, worktree: string, branch: string, options?: {
|
|
190
|
+
lease?: string;
|
|
191
|
+
}): Promise<void>;
|
|
192
|
+
/** Open the pull request that stands for the branch, against the
|
|
193
|
+
* repository's own default branch, and say which worker role asked —
|
|
194
|
+
* visibly, in the body a human reads. The orchestrator asks only when
|
|
195
|
+
* no open pull request stands for the branch (its `prForBranch` said
|
|
196
|
+
* so, or the last one closed); opening a second is the platform's to
|
|
197
|
+
* refuse, and its refusal is said, not smoothed over. */
|
|
198
|
+
openPr(repo: RepoRef, branch: string, pr: {
|
|
199
|
+
author: string;
|
|
200
|
+
title: string;
|
|
201
|
+
body: string;
|
|
202
|
+
}): Promise<PrFacts>;
|
|
203
|
+
/** Bring **all** the remote's refs into the checkout's own remote refs —
|
|
204
|
+
* fetch with prune, refs only, nothing checked out (`aivi_sync`: the
|
|
205
|
+
* worker looks before it rebases, and the push's fresh view of every
|
|
206
|
+
* branch starts here). A failed transfer throws; there is nothing a
|
|
207
|
+
* caller could safely do with a stale view. */
|
|
208
|
+
fetchRefs(repo: RepoRef, directory: string): Promise<void>;
|
|
209
|
+
}
|
|
210
|
+
/** The outcome of bringing a checkout up to date, in the forge's own words:
|
|
211
|
+
* `updated` when it moved, `current` when it already matched, `held` with
|
|
212
|
+
* the reason when only a human can decide. */
|
|
213
|
+
export interface ForgeSync {
|
|
214
|
+
state: 'updated' | 'current' | 'held';
|
|
215
|
+
from?: string;
|
|
216
|
+
to?: string;
|
|
217
|
+
reason?: string;
|
|
218
|
+
}
|
|
219
|
+
/** The registry's answer: the forge that recognised the project's remote,
|
|
220
|
+
* and the repository it recognised — handed straight to `syncSource`,
|
|
221
|
+
* `push` and friends, never inspected outside the forge. */
|
|
222
|
+
export interface ForgeOwner {
|
|
223
|
+
forge: Forge;
|
|
224
|
+
repo: RepoRef;
|
|
225
|
+
}
|
|
226
|
+
/** The host-side forge registry (built 2026-10-02, the first thing with the
|
|
227
|
+
* question "who owns this project's remote?" to ask). Forge modules
|
|
228
|
+
* register once at module start, the way channels register with the router
|
|
229
|
+
* and tools claim on the tool surface; the host's machinery asks.
|
|
230
|
+
*
|
|
231
|
+
* **The answer may be nobody** (ruled 2026-09-29: the forge is a
|
|
232
|
+
* configurable path, not the spine): with no forge registered, or a remote
|
|
233
|
+
* no registered forge recognises, `owner` answers undefined and the
|
|
234
|
+
* machinery does the local-only thing it has always done. Nothing here
|
|
235
|
+
* reaches a network: recognising is a read of `origin`, and reaching
|
|
236
|
+
* `origin` is what the returned forge authenticates for. */
|
|
237
|
+
export interface Forges {
|
|
238
|
+
/** Register once at module start; the returned function unregisters. */
|
|
239
|
+
register(forge: Forge): () => void;
|
|
240
|
+
/** Who owns this project's remote: the first registered forge whose
|
|
241
|
+
* `repoFor` answers with a repository. Undefined when none does. */
|
|
242
|
+
owner(project: ForgeProject): Promise<ForgeOwner | undefined>;
|
|
243
|
+
}
|
package/dist/forge.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **forge** contract: the seam between aivi and a repository host. A
|
|
3
|
+
* forge owns the *remote*: the repository a project's checkout came from, the
|
|
4
|
+
* pull requests over its branches, the review conversation on them, and the
|
|
5
|
+
* credential that lets aivi move bytes as its own app.
|
|
6
|
+
*
|
|
7
|
+
* The line between a forge and aivi is **remote, not git** (ruled
|
|
8
|
+
* 2026-09-29): any git operation that reaches `origin` authenticates to it,
|
|
9
|
+
* so it belongs to the forge — clone, fetch, the sync of a clean checkout,
|
|
10
|
+
* push. What only touches the local store is aivi's own machinery (the
|
|
11
|
+
* orchestrator creates and reaps worktrees, the worker commits in one) and
|
|
12
|
+
* never appears here. A forge uses its API for what git cannot see: pull
|
|
13
|
+
* requests and reviews.
|
|
14
|
+
*
|
|
15
|
+
* Types only — the machinery consuming them ships in the host. The exact
|
|
16
|
+
* member set is **provisional by design**
|
|
17
|
+
* ([forge-github.md](../../docs/plans/forge-github.md)): the orchestrator's
|
|
18
|
+
* wiring is what proves it, and more forges will move the details.
|
|
19
|
+
*
|
|
20
|
+
* A project **having no forge is not a failure** (ruled 2026-09-29): a ticket
|
|
21
|
+
* can be "research X, write it up" and complete without aivi ever asking a
|
|
22
|
+
* repository host anything. Nothing here may assume a worker touches a remote.
|
|
23
|
+
*
|
|
24
|
+
* Cloning is a forge's operation too, but it is not on this interface: it
|
|
25
|
+
* happens once, at project setup, through the plugin's `./setupProject`
|
|
26
|
+
* contributor ([setup-project.ts](setup-project.ts)), which is where a person
|
|
27
|
+
* is asked which repository they mean. By the time a `Forge` is speaking to
|
|
28
|
+
* aivi the checkout already exists.
|
|
29
|
+
*/
|
|
30
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kit: the contracts aivi is built on, in the package that owns them.
|
|
3
|
+
* The **run** domain (what a run is), the **tracker** contract (the stages
|
|
4
|
+
* the orchestrator walks and the platform behind them), the **channel**
|
|
5
|
+
* contract (conversations on chat platforms), and the **module** contract
|
|
6
|
+
* (what `runHost` composes and hands a module) are declared here; the host
|
|
7
|
+
* implements them and imports them from here, and plugin packages import
|
|
8
|
+
* them from here. One copy, owned by the package everyone already imports.
|
|
9
|
+
*
|
|
10
|
+
* This package depends on nothing above it: `@aivi/core` for the shared
|
|
11
|
+
* facts, `@opencode/client` to name the client plugins receive. The host is
|
|
12
|
+
* the one that depends on the kit, never the other way — at compile time or
|
|
13
|
+
* at runtime. Where a contract names a store or a registry, it names an
|
|
14
|
+
* interface here and the host's class is checked against it.
|
|
15
|
+
*/
|
|
16
|
+
export * from './cli.ts';
|
|
17
|
+
export * from './module.ts';
|
|
18
|
+
export * from './plugin.ts';
|
|
19
|
+
export * from './run.ts';
|
|
20
|
+
export * from './setup.ts';
|
|
21
|
+
export * from './setup-project.ts';
|
|
22
|
+
export * from './tracker.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kit: the contracts aivi is built on, in the package that owns them.
|
|
3
|
+
* The **run** domain (what a run is), the **tracker** contract (the stages
|
|
4
|
+
* the orchestrator walks and the platform behind them), the **channel**
|
|
5
|
+
* contract (conversations on chat platforms), and the **module** contract
|
|
6
|
+
* (what `runHost` composes and hands a module) are declared here; the host
|
|
7
|
+
* implements them and imports them from here, and plugin packages import
|
|
8
|
+
* them from here. One copy, owned by the package everyone already imports.
|
|
9
|
+
*
|
|
10
|
+
* This package depends on nothing above it: `@aivi/core` for the shared
|
|
11
|
+
* facts, `@opencode/client` to name the client plugins receive. The host is
|
|
12
|
+
* the one that depends on the kit, never the other way — at compile time or
|
|
13
|
+
* at runtime. Where a contract names a store or a registry, it names an
|
|
14
|
+
* interface here and the host's class is checked against it.
|
|
15
|
+
*/
|
|
16
|
+
export * from './cli.js';
|
|
17
|
+
export * from './module.js';
|
|
18
|
+
export * from './plugin.js';
|
|
19
|
+
export * from './run.js';
|
|
20
|
+
export * from './setup.js';
|
|
21
|
+
export * from './setup-project.js';
|
|
22
|
+
export * from './tracker.js';
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **module contract**: what aivi composes and hands a module at start,
|
|
3
|
+
* and what the module may do with it. Declared here because both sides
|
|
4
|
+
* follow it: the host's `runHost` builds the bag and the modules use it; the
|
|
5
|
+
* host's classes are checked against the interfaces below, so the contract
|
|
6
|
+
* and the machinery cannot drift apart silently.
|
|
7
|
+
*
|
|
8
|
+
* What a module receives is enough to serve and nothing to drive: the run
|
|
9
|
+
* vocabulary (`./run.ts`) is read-only from here, the tracker stages
|
|
10
|
+
* (`./tracker.ts`) are what a tracker hands *in*, and the OpenCode client
|
|
11
|
+
* arrives discovered on every call — a restarted service is picked up by the
|
|
12
|
+
* next unit of work without restarting aivi.
|
|
13
|
+
*/
|
|
14
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
15
|
+
import type { AddJobOptions, AuditEntry, Config, ExecutionContext, ExecutionResult, Job, JobEntry, KnowledgeService, LoadedConfig, Logger, Person, PersonToken, Report, RequestLogEntry, Run, RunFilter, RunState, Task, ToolDescriptor } from '@aivi/core';
|
|
16
|
+
import type { OpenCode } from '@opencode/client';
|
|
17
|
+
import type { Channels } from './channel.ts';
|
|
18
|
+
import type { Dispatcher } from './dispatcher.ts';
|
|
19
|
+
import type { Forges } from './forge.ts';
|
|
20
|
+
import type { RunView } from './run.ts';
|
|
21
|
+
import type { Tracker } from './tracker.ts';
|
|
22
|
+
import type { Voice } from './voice.ts';
|
|
23
|
+
/** The OpenCode client a module receives from `services.opencode()`: the
|
|
24
|
+
* SDK's own client, named here because this is where plugins meet it. */
|
|
25
|
+
export type OpenCodeClient = ReturnType<typeof OpenCode.make>;
|
|
26
|
+
/** A tool failed with an answer for the agent: this status and message reach the caller. */
|
|
27
|
+
export declare class ToolError extends Error {
|
|
28
|
+
readonly status: number;
|
|
29
|
+
constructor(status: number, message: string);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A module start that no retry can fix: a missing token, an id that does not
|
|
33
|
+
* match. Modules throw this for what the operator must change; everything else
|
|
34
|
+
* (a platform answering 503, a network blip) is retried.
|
|
35
|
+
*/
|
|
36
|
+
export declare class ConfigurationError extends Error {
|
|
37
|
+
}
|
|
38
|
+
/** What a module hands back at start: the one thing the host may do to it is stop it. */
|
|
39
|
+
export interface RunningModule {
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/** The module a plugin package builds: an id the operator configured, a start, and optional system jobs. */
|
|
43
|
+
export interface AiviModule {
|
|
44
|
+
/** The module id — the package's short name (`channel-discord`), the same
|
|
45
|
+
* word as `plugins.<id>`. It keys the supervisor, shows as the `/status`
|
|
46
|
+
* id and owns task claims. Not the channel's platform id: that one is
|
|
47
|
+
* `discord` and keys the database, and a module that registers a channel
|
|
48
|
+
* carries both on purpose. */
|
|
49
|
+
id: string;
|
|
50
|
+
start(services: AiviServices): Promise<RunningModule>;
|
|
51
|
+
/**
|
|
52
|
+
* System jobs to seed beside the host's own while this module is composed.
|
|
53
|
+
* They carry `invocation` tasks for operations the module claims in its
|
|
54
|
+
* `start`; when the module leaves the composition, its jobs are removed
|
|
55
|
+
* with it.
|
|
56
|
+
*/
|
|
57
|
+
jobs?(config: Config): Job[];
|
|
58
|
+
}
|
|
59
|
+
/** Everything the host composes and a module may use. */
|
|
60
|
+
export interface AiviServices {
|
|
61
|
+
loaded: LoadedConfig;
|
|
62
|
+
store: Store;
|
|
63
|
+
knowledge: KnowledgeService;
|
|
64
|
+
/** Discovers the OpenCode service on every call. Call once per unit of work and hold the client for its duration. */
|
|
65
|
+
opencode: () => Promise<OpenCodeClient>;
|
|
66
|
+
/** The host's one OpenCode event stream, fanned out by session id; channel progress watches turns through it. */
|
|
67
|
+
events: SessionEvents;
|
|
68
|
+
signal: AbortSignal;
|
|
69
|
+
log: Logger;
|
|
70
|
+
/** Chat platform modules register here once; that makes them report destinations and session owners. */
|
|
71
|
+
channels: Channels;
|
|
72
|
+
/** Webhook routes a module exposes on the host listener, outside bearer auth; the platform's signature is the auth. */
|
|
73
|
+
routes: PublicRoutes;
|
|
74
|
+
/** What `kind: 'invocation'` tasks dispatch to: the host claims its own operations here, modules claim theirs. */
|
|
75
|
+
tasks: TaskClaims;
|
|
76
|
+
/** The tool surface the OpenCode plugin registers at load: the host claims its own tools here, modules claim theirs. */
|
|
77
|
+
tools: ToolClaims;
|
|
78
|
+
/** The forge registry: a forge module registers its `Forge` once at start;
|
|
79
|
+
* the host's machinery asks who owns a project's remote. A project with no
|
|
80
|
+
* forge is not a failure — the answer is simply nobody. */
|
|
81
|
+
forges: Forges;
|
|
82
|
+
/** The voice seat: the voice module provides its one adapter at start;
|
|
83
|
+
* modules standing in a room (discord, meet, …) open `room` handles and
|
|
84
|
+
* feed audio. No adapter loaded, no hearing: `room` throws VoiceNotLoaded
|
|
85
|
+
* naming the fix — the plugin is optional and the failure is honest. */
|
|
86
|
+
voice: Voice;
|
|
87
|
+
/** Capacity as named pools: every service that works asks for a slot before
|
|
88
|
+
* working (the ticket workers drew it first; voice rooms draw the same).
|
|
89
|
+
* A pool is an endpoint — capacity, an optional model, an optional
|
|
90
|
+
* fallback. Pools are how a local box and a remote endpoint stay apart. */
|
|
91
|
+
dispatcher: Dispatcher;
|
|
92
|
+
/** Ticket-to-run machinery: a tracker module registers its stages, stops
|
|
93
|
+
* runs and reads run records here. The orchestrator owns the run's state
|
|
94
|
+
* machine, the worker session, and the worker tools; it never calls back. */
|
|
95
|
+
orchestrator: Orchestrator;
|
|
96
|
+
/** Tell the scheduler and every channel engine that the queue or capacity changed; dispatch now. */
|
|
97
|
+
wake(): void;
|
|
98
|
+
/** Be told the same; a channel engine ticks on it instead of polling for capacity released elsewhere. */
|
|
99
|
+
onWake(listener: () => void): () => void;
|
|
100
|
+
/** Abort the whole host. Only for failures the module cannot recover from. */
|
|
101
|
+
fail(error: unknown): void;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The home store as a module meets it: the whole contract of the host's
|
|
105
|
+
* `Store` class, which is checked against this interface. A module gets the
|
|
106
|
+
* open database for its own tables (namespaced migrations), the job and run
|
|
107
|
+
* surface its commands report through, the leases it reads, the identity
|
|
108
|
+
* lookups the chat platforms redeem, and the request diary a setup flow
|
|
109
|
+
* watches. No module ever names the database file: the runner opens it and
|
|
110
|
+
* hands over the handle.
|
|
111
|
+
*/
|
|
112
|
+
export interface Store {
|
|
113
|
+
readonly db: DatabaseSync;
|
|
114
|
+
/** Open for the last time; closing it is the runner's, never a module's. */
|
|
115
|
+
close(): void;
|
|
116
|
+
/** Apply this namespace's migrations, once, in order; new tables belong to the module that named them. */
|
|
117
|
+
migrate(namespace: string, steps: readonly string[]): void;
|
|
118
|
+
/** Run one SQLite transaction; a throw rolls it back whole. */
|
|
119
|
+
transaction<T>(fn: () => T): T;
|
|
120
|
+
/** Leave one audit line on a run. */
|
|
121
|
+
note(id: string, action: string, reason: string, now?: number): void;
|
|
122
|
+
/** Run history: one job, one state, the newest few. */
|
|
123
|
+
runs(filter?: RunFilter): Run[];
|
|
124
|
+
/** One run by id; throws when it is not there. */
|
|
125
|
+
run(id: string): Run;
|
|
126
|
+
/** How many runs stand in each state. */
|
|
127
|
+
counts(): Record<RunState, number>;
|
|
128
|
+
/** One run of a task that should happen once now (a report, a re-entry). */
|
|
129
|
+
enqueue(task: Task, resource: string, dedupeKey: string, now?: number, report?: Report | null): Run;
|
|
130
|
+
/** Every job as the store holds it: definition, source, state, next instant. */
|
|
131
|
+
jobs(): JobEntry[];
|
|
132
|
+
/** The job defined under `id`; throws when it is not there. */
|
|
133
|
+
job(id: string): JobEntry;
|
|
134
|
+
/** Reconcile the definitions with `config.json` and the host's system jobs. */
|
|
135
|
+
syncJobs(config: Job[], system?: Job[], now?: number): void;
|
|
136
|
+
/** Define a job from an agent tool call or the operator CLI. */
|
|
137
|
+
addJob(spec: Job, source: 'agent' | 'operator', now?: number, options?: AddJobOptions): JobEntry;
|
|
138
|
+
setJobEnabled(id: string, enabled: boolean, now?: number): JobEntry;
|
|
139
|
+
removeJob(id: string, now?: number): void;
|
|
140
|
+
/** Run a job now, beside its schedule. */
|
|
141
|
+
runJob(id: string, now?: number): Run;
|
|
142
|
+
/** Materialize occurrences that came due while aivi slept; silence is a result. */
|
|
143
|
+
materializeDue(now?: number, graceMs?: number): {
|
|
144
|
+
created: number;
|
|
145
|
+
missed: Run[];
|
|
146
|
+
};
|
|
147
|
+
/** The next occurrence to materialize; null when nothing is due. */
|
|
148
|
+
nextDue(now?: number): number | null;
|
|
149
|
+
/** Name the session a claimed run opened, so reports can find their way back. */
|
|
150
|
+
attachSession(id: string, owner: string, sessionId: string): void;
|
|
151
|
+
/** Settle a claimed run: its result, its state, its reason. */
|
|
152
|
+
finish(id: string, owner: string, state: 'succeeded' | 'failed' | 'blocked', result: unknown, reason: string, now?: number): void;
|
|
153
|
+
cancelQueued(id: string, now?: number): void;
|
|
154
|
+
/** A person answered what a blocked run awaited; the run resumes its outcome. */
|
|
155
|
+
resolveBlocked(id: string, outcome: 'succeeded' | 'failed', reason: string, now?: number): void;
|
|
156
|
+
/** The one live host: a second serve finds the daemon held and steps back. */
|
|
157
|
+
acquireDaemon(owner: string, pid?: number, now?: number): void;
|
|
158
|
+
releaseDaemon(owner: string): void;
|
|
159
|
+
/** A run's audit trail, oldest first. */
|
|
160
|
+
history(id: string): AuditEntry[];
|
|
161
|
+
/** Runs that reached a final state since `since`, newest first. */
|
|
162
|
+
recent(since: number, limit?: number): Run[];
|
|
163
|
+
lastRun(jobId: string): Run | null;
|
|
164
|
+
/** Consecutive failed runs of a job; the streak a report's nudge reads. */
|
|
165
|
+
failureStreak(jobId: string): number;
|
|
166
|
+
/** Tell a running run a person asked it to stop. */
|
|
167
|
+
requestCancel(id: string, now?: number): void;
|
|
168
|
+
/** The runs this owner was told to stop and has not felt yet. */
|
|
169
|
+
cancelRequested(owner: string): string[];
|
|
170
|
+
/** Drop history older than the instant; the counts say what left. */
|
|
171
|
+
prune(olderThan: number): {
|
|
172
|
+
runs: number;
|
|
173
|
+
jobs: number;
|
|
174
|
+
linkCodes: number;
|
|
175
|
+
};
|
|
176
|
+
/** The people directory: `aivi people`, and the tokens they sign in with. */
|
|
177
|
+
createPerson(spec: {
|
|
178
|
+
name: string;
|
|
179
|
+
email?: string | null;
|
|
180
|
+
roles?: string[];
|
|
181
|
+
}, now?: number): Person;
|
|
182
|
+
person(id: string): Person | null;
|
|
183
|
+
people(): Person[];
|
|
184
|
+
mintToken(personId: string, label: string, now?: number): {
|
|
185
|
+
token: PersonToken;
|
|
186
|
+
secret: string;
|
|
187
|
+
};
|
|
188
|
+
personForToken(secret: string): {
|
|
189
|
+
person: Person;
|
|
190
|
+
token: PersonToken;
|
|
191
|
+
} | null;
|
|
192
|
+
/** A link code a person spends on a chat platform; the expiry is the answer. */
|
|
193
|
+
mintLinkCode(personId: string, ttlMs?: number, now?: number): {
|
|
194
|
+
code: string;
|
|
195
|
+
expiresAt: number;
|
|
196
|
+
};
|
|
197
|
+
redeemLinkCode(code: string, channel: string, userId: string, now?: number): {
|
|
198
|
+
reason: 'bound';
|
|
199
|
+
person: Person;
|
|
200
|
+
} | {
|
|
201
|
+
reason: 'already';
|
|
202
|
+
person: Person;
|
|
203
|
+
} | {
|
|
204
|
+
reason: 'expired';
|
|
205
|
+
} | {
|
|
206
|
+
reason: 'unknown';
|
|
207
|
+
};
|
|
208
|
+
/** The person behind a platform identity, if aivi knows one. */
|
|
209
|
+
identityFor(channel: string, userId: string): Person | null;
|
|
210
|
+
personLinkedIn(channel: string, personId: string): boolean;
|
|
211
|
+
/** Record one call the host answered; the diary a setup flow watches. */
|
|
212
|
+
logRequest(entry: Omit<RequestLogEntry, 'id'>): void;
|
|
213
|
+
requests(filter?: {
|
|
214
|
+
sinceId?: number;
|
|
215
|
+
method?: string;
|
|
216
|
+
path?: string;
|
|
217
|
+
limit?: number;
|
|
218
|
+
}): RequestLogEntry[];
|
|
219
|
+
clearRequests(olderThan: number): number;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The run machinery as a module meets it: a tracker registers its stages,
|
|
223
|
+
* wakes the walk, stops and answers runs, and reads run records. Work is
|
|
224
|
+
* never handed in here — the walk over the tracker's board is the only door
|
|
225
|
+
* for board work (ruled 2026-10-01). The orchestrator itself is the host's
|
|
226
|
+
* class, checked against this interface.
|
|
227
|
+
*/
|
|
228
|
+
export interface Orchestrator {
|
|
229
|
+
/** Register the one tracker a module speaks for. */
|
|
230
|
+
addTracker(tracker: Tracker): void;
|
|
231
|
+
/** Walk the board now: the whole set, or one project. */
|
|
232
|
+
wake(projectId?: string): Promise<void>;
|
|
233
|
+
/** Stop a running run; the run ends `cancelled` and says so where people read. */
|
|
234
|
+
stop(runId: string, reason: string): Promise<void>;
|
|
235
|
+
/** A person's answer to a parked run; `formId` is the OpenCode form it answers. */
|
|
236
|
+
answer(sessionId: string, text: string, formId?: string): Promise<{
|
|
237
|
+
resumed: true;
|
|
238
|
+
} | {
|
|
239
|
+
resumed: false;
|
|
240
|
+
queued?: string;
|
|
241
|
+
refused?: string;
|
|
242
|
+
}>;
|
|
243
|
+
/** The run working `ticketId` for `trackerId`, if one is in flight. */
|
|
244
|
+
activeRun(trackerId: string, ticketId: string): RunView | undefined;
|
|
245
|
+
/** The run a worker session belongs to, whatever tracker spoke for it. */
|
|
246
|
+
runBySession(sessionId: string): RunView | undefined;
|
|
247
|
+
/** Forget a waiting request: a person moved the ticket themselves. */
|
|
248
|
+
cancelWaiting(trackerId: string, ticketId: string): void;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* What a public route sees: the request and its raw body (signatures are
|
|
252
|
+
* computed over bytes, never re-serialized JSON). Headers are the plain
|
|
253
|
+
* lower-cased view the API has always handed over.
|
|
254
|
+
*/
|
|
255
|
+
export interface PublicRequest {
|
|
256
|
+
method: string;
|
|
257
|
+
headers: Record<string, string | undefined>;
|
|
258
|
+
body: Buffer;
|
|
259
|
+
}
|
|
260
|
+
export type PublicRouteHandler = (request: PublicRequest) => Promise<{
|
|
261
|
+
status: number;
|
|
262
|
+
body?: unknown;
|
|
263
|
+
}>;
|
|
264
|
+
/**
|
|
265
|
+
* Routes a module exposes without the bearer token: webhooks from platforms
|
|
266
|
+
* that authenticate with their own signature. A path is owned by one handler;
|
|
267
|
+
* registering it twice is a programming error. `/health` and `/version` are
|
|
268
|
+
* the only other unauthenticated routes, and no module may take them. The
|
|
269
|
+
* registry is the host's class, checked against this interface.
|
|
270
|
+
*/
|
|
271
|
+
export interface PublicRoutes {
|
|
272
|
+
register(path: string, handler: PublicRouteHandler): () => void;
|
|
273
|
+
}
|
|
274
|
+
export type TaskHandler = (run: Run, context: ExecutionContext) => Promise<ExecutionResult>;
|
|
275
|
+
/** What `kind: 'invocation'` tasks dispatch to: the host claims its own
|
|
276
|
+
* operations here, modules claim theirs. */
|
|
277
|
+
export interface TaskClaims {
|
|
278
|
+
claim(name: string, handler: TaskHandler): void;
|
|
279
|
+
release(name: string): void;
|
|
280
|
+
}
|
|
281
|
+
/** One call as the generic dispatch hands it over: the envelope's ids, the model's input. */
|
|
282
|
+
export interface ToolCall {
|
|
283
|
+
sessionId: string;
|
|
284
|
+
messageId?: string;
|
|
285
|
+
input: Record<string, unknown>;
|
|
286
|
+
}
|
|
287
|
+
export type ToolHandler = (call: ToolCall) => Promise<unknown>;
|
|
288
|
+
/** The tool surface the OpenCode plugin registers at load: the host claims
|
|
289
|
+
* its own tools here, modules claim theirs. */
|
|
290
|
+
export interface ToolClaims {
|
|
291
|
+
claim(descriptor: ToolDescriptor, handler: ToolHandler): void;
|
|
292
|
+
release(id: string): void;
|
|
293
|
+
}
|
|
294
|
+
/** The shape every OpenCode event shares; session events carry `data.sessionID`. */
|
|
295
|
+
export interface SessionEvent {
|
|
296
|
+
type: string;
|
|
297
|
+
data?: {
|
|
298
|
+
sessionID?: string;
|
|
299
|
+
} & Record<string, unknown>;
|
|
300
|
+
}
|
|
301
|
+
export type SessionEventListener = (event: SessionEvent) => void;
|
|
302
|
+
/** The host's one OpenCode event stream, fanned out by session id. */
|
|
303
|
+
export interface SessionEvents {
|
|
304
|
+
/** Receive this session's events until the returned function is called. */
|
|
305
|
+
watch(sessionID: string, listener: SessionEventListener): () => void;
|
|
306
|
+
}
|
package/dist/module.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **module contract**: what aivi composes and hands a module at start,
|
|
3
|
+
* and what the module may do with it. Declared here because both sides
|
|
4
|
+
* follow it: the host's `runHost` builds the bag and the modules use it; the
|
|
5
|
+
* host's classes are checked against the interfaces below, so the contract
|
|
6
|
+
* and the machinery cannot drift apart silently.
|
|
7
|
+
*
|
|
8
|
+
* What a module receives is enough to serve and nothing to drive: the run
|
|
9
|
+
* vocabulary (`./run.ts`) is read-only from here, the tracker stages
|
|
10
|
+
* (`./tracker.ts`) are what a tracker hands *in*, and the OpenCode client
|
|
11
|
+
* arrives discovered on every call — a restarted service is picked up by the
|
|
12
|
+
* next unit of work without restarting aivi.
|
|
13
|
+
*/
|
|
14
|
+
/** A tool failed with an answer for the agent: this status and message reach the caller. */
|
|
15
|
+
export class ToolError extends Error {
|
|
16
|
+
status;
|
|
17
|
+
constructor(status, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.status = status;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A module start that no retry can fix: a missing token, an id that does not
|
|
24
|
+
* match. Modules throw this for what the operator must change; everything else
|
|
25
|
+
* (a platform answering 503, a network blip) is retried.
|
|
26
|
+
*/
|
|
27
|
+
export class ConfigurationError extends Error {
|
|
28
|
+
}
|