@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
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **tracker** contract, in the two seams a tracker module lives between.
|
|
3
|
+
*
|
|
4
|
+
* `Tracker` is the seam between the host's orchestrator and the module: the
|
|
5
|
+
* **stages** a run is walked through, keyed by the run. The orchestrator
|
|
6
|
+
* walks them in order and awaits only the lifecycle ones; it never learns
|
|
7
|
+
* what a ticket platform is. `Platform` is the seam between the module and
|
|
8
|
+
* its ticket platform: the conversation-keyed translator that turns the
|
|
9
|
+
* platform's events into neutral ones and aivi's neutral updates into the
|
|
10
|
+
* platform's mutations. A module registers one `Tracker` with the
|
|
11
|
+
* orchestrator and speaks its `Platform` to whomever on its platform; the
|
|
12
|
+
* stages that render are usually one-line forwards — the division is honest,
|
|
13
|
+
* not duplicated.
|
|
14
|
+
*
|
|
15
|
+
* The words here are platform-neutral by design: roughly seven trackers will
|
|
16
|
+
* move this contract, and every entry earns its place with a second
|
|
17
|
+
* tracker's need. The run shapes the stages carry are the shared run domain
|
|
18
|
+
* (`./run.ts`); the ticket shapes here are what the tracker reports and the
|
|
19
|
+
* orchestrator routes.
|
|
20
|
+
*/
|
|
21
|
+
import type { RunPlan, RunQuestion, RunView } from './run.ts';
|
|
22
|
+
/**
|
|
23
|
+
* The work a tracker answers for: the **stages** the orchestrator walks a
|
|
24
|
+
* run through, and the board it walks for work. Division of labour, said as
|
|
25
|
+
* an interface: the orchestrator orchestrates and never learns what a ticket
|
|
26
|
+
* platform is; the tracker tracks and answers for its platform alone. A
|
|
27
|
+
* tracker module registers ONE of these at start; the orchestrator calls the
|
|
28
|
+
* stages in the documented order and awaits only the lifecycle ones
|
|
29
|
+
* (`initWork`, `endWork`): their failure is the run's failure, said visibly.
|
|
30
|
+
* The renders (`ready`, `startWork`, `question`, `plan`) are fire-and-forget
|
|
31
|
+
* — a platform that cannot show a thing loses nothing the orchestrator cares
|
|
32
|
+
* about, and a tracker retries its own renders in its own time. The order is
|
|
33
|
+
* owned by `docs/orchestrator.md` ("The tracker's stages").
|
|
34
|
+
*
|
|
35
|
+
* `initWork` is where a tracker does whatever its platform needs to open a
|
|
36
|
+
* ticket to work — Linear delegates the issue to its own app and Linear's
|
|
37
|
+
* answer carries the new agent session — and it hands back the ticket's
|
|
38
|
+
* **summary**: the words the worker is started with, composed around the
|
|
39
|
+
* directory and the lane by the orchestrator, which never reads a ticket
|
|
40
|
+
* itself. A tracker that needs nothing answers with the ticket's text alone.
|
|
41
|
+
*/
|
|
42
|
+
/** What `initWork` answers with: the words the worker is started with, and
|
|
43
|
+
* the ticket's **branch name** where the platform names one. A lane that
|
|
44
|
+
* writes (`worktree: true`) gets its worktree on this branch, and the
|
|
45
|
+
* orchestrator never invents a name — such a lane whose tracker answers
|
|
46
|
+
* without one fails the run visibly. A tracker whose platform has no
|
|
47
|
+
* branch answers with the summary alone. */
|
|
48
|
+
export interface WorkEntry {
|
|
49
|
+
summary: string;
|
|
50
|
+
branch?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface Tracker {
|
|
53
|
+
/** The module id this tracker speaks for (`tracker-linear`): the key
|
|
54
|
+
* the orchestrator records claims and queue places under. */
|
|
55
|
+
readonly id: string;
|
|
56
|
+
/** The projects this tracker speaks for, by core id: what a walk
|
|
57
|
+
* visits. */
|
|
58
|
+
projects(): string[];
|
|
59
|
+
/** Tickets sitting in a lane, in the board's own top-to-bottom order.
|
|
60
|
+
* `blocked` says a person's move is awaited, whatever the platform
|
|
61
|
+
* calls it; blocked tickets wait and are never claimed. */
|
|
62
|
+
tickets(projectId: string, lane: string): Promise<{
|
|
63
|
+
id: string;
|
|
64
|
+
blocked: boolean;
|
|
65
|
+
}[]>;
|
|
66
|
+
/** Move a ticket into a lane: the queue pickup says it before the
|
|
67
|
+
* worker starts, an ending moves the ticket where the lane order put
|
|
68
|
+
* it. The orchestrator's decision; the tracker performs it. */
|
|
69
|
+
moveTo(projectId: string, ticketId: string, lane: string): Promise<void>;
|
|
70
|
+
/** Which lane the ticket sits in **right now**, by name; undefined when it
|
|
71
|
+
* is gone from this board — deleted, or on a team the project does not
|
|
72
|
+
* map. The orchestrator's validity check before an ending move (ruled
|
|
73
|
+
* 2026-10-02): a person may have moved the ticket out from under a live
|
|
74
|
+
* run while a webhook was missed; their move wins, and the owed move is
|
|
75
|
+
* spent rather than undone. */
|
|
76
|
+
ticketLane(projectId: string, ticketId: string): Promise<string | undefined>;
|
|
77
|
+
/** The **ticket desk**: a worker's hand on the ticket itself, with no
|
|
78
|
+
* conversation to speak through. The host claims the `aivi_ticket_*` ids
|
|
79
|
+
* exactly once and routes every call to the desk of the tracker that works
|
|
80
|
+
* the calling run's ticket — so several trackers can run in one home and a
|
|
81
|
+
* worker never names a ticket. Required of every tracker: the desk is
|
|
82
|
+
* ticket work, and a tracker that cannot do ticket work is not a tracker.
|
|
83
|
+
* Each method receives the run, never a bare id: the ticket is the run's,
|
|
84
|
+
* and the ground truth of who may touch it is the ledger's. */
|
|
85
|
+
desk: TicketDesk;
|
|
86
|
+
/** The dispatcher's slot is in hand and the run is claimed: open the
|
|
87
|
+
* ticket to work on the platform and answer with the **work entry**:
|
|
88
|
+
* the summary the worker is started with, and the **branch the ticket
|
|
89
|
+
* works on** when the platform names one (Linear's `Issue.branchName`).
|
|
90
|
+
* A lane that writes gets its worktree on that branch, and the
|
|
91
|
+
* orchestrator never invents a name — a worktree lane whose tracker
|
|
92
|
+
* answers without one fails the run visibly. Linear's delegate
|
|
93
|
+
* mutation creates the agent session and its answer serves as the
|
|
94
|
+
* summary; the tracker stores the pair and may already post a first
|
|
95
|
+
* word ("preparing the workspace"). Failure fails the run — visibly,
|
|
96
|
+
* and the slot goes back. AWAITED. */
|
|
97
|
+
initWork(run: RunView): Promise<WorkEntry>;
|
|
98
|
+
/** The worker's session exists and its work environment is ready:
|
|
99
|
+
* where a tracker attaches the run to the pair it opened at
|
|
100
|
+
* `initWork`. A render. */
|
|
101
|
+
ready(run: RunView): void | Promise<void>;
|
|
102
|
+
/** The task went into the session and work is turning. Optional: a
|
|
103
|
+
* platform whose session already shows life (Linear watches its own
|
|
104
|
+
* agent sessions) says nothing here. */
|
|
105
|
+
startWork?(run: RunView): void | Promise<void>;
|
|
106
|
+
/** The worker asked a person a question and parked. **Required of
|
|
107
|
+
* every tracker**: the OpenCode form is the durable record of the
|
|
108
|
+
* wait, but only the tracker knows what a question looks like on its
|
|
109
|
+
* platform. A render. */
|
|
110
|
+
question(run: RunView, question: RunQuestion): void | Promise<void>;
|
|
111
|
+
/** The worker's working plan, whole as it stands. Optional: a platform
|
|
112
|
+
* without a plan surface drops it. A render. */
|
|
113
|
+
plan?(run: RunView, plan: RunPlan): void | Promise<void>;
|
|
114
|
+
/** The run ended: say so where people read — the closing words in the
|
|
115
|
+
* shape the platform gives endings (Linear responds its agent
|
|
116
|
+
* session, which ends it). The move to the next lane is the
|
|
117
|
+
* orchestrator's and happens after this returns; the lease returns
|
|
118
|
+
* last. A permanent failure here does not hold the ticket: the
|
|
119
|
+
* ending failed is said and the person is asked for help (a tracker
|
|
120
|
+
* marks its ticket for a human, in its platform's words), while the
|
|
121
|
+
* closing stays owed to the tracker's own retries. AWAITED. */
|
|
122
|
+
endWork(run: RunView): Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
/** One workflow state of a team, as the platform holds it. `type` is the
|
|
125
|
+
* platform's own flavor word — Linear's `completed`/`canceled` are finished
|
|
126
|
+
* work; the machinery asks the adapter what a finished state means and
|
|
127
|
+
* never compares raw words of one platform. */
|
|
128
|
+
export interface TrackerState {
|
|
129
|
+
id: string;
|
|
130
|
+
name: string;
|
|
131
|
+
type: string;
|
|
132
|
+
}
|
|
133
|
+
/** A ticket, in the neutral shape the machinery reads. Every field exists
|
|
134
|
+
* because the routing decisions read it; `state` is the ticket's current
|
|
135
|
+
* lane, `teamId` is what routes a ticket to a project. */
|
|
136
|
+
export interface TrackerIssue {
|
|
137
|
+
id: string;
|
|
138
|
+
/** The human-facing id (`ENG-1`): what people read and comments quote. */
|
|
139
|
+
identifier: string;
|
|
140
|
+
title: string;
|
|
141
|
+
description: string | null;
|
|
142
|
+
/** The branch the platform suggests for this ticket; empty when it has no opinion. */
|
|
143
|
+
branchName: string;
|
|
144
|
+
url: string;
|
|
145
|
+
state: TrackerState;
|
|
146
|
+
teamId: string;
|
|
147
|
+
labels: {
|
|
148
|
+
id: string;
|
|
149
|
+
name: string;
|
|
150
|
+
}[];
|
|
151
|
+
/** Whose user id the ticket is delegated to; null when nobody's. */
|
|
152
|
+
delegateId: string | null;
|
|
153
|
+
assignee: {
|
|
154
|
+
id: string;
|
|
155
|
+
name: string;
|
|
156
|
+
} | null;
|
|
157
|
+
/** True once the platform has deleted the ticket as far as work goes;
|
|
158
|
+
* a dead ticket gets nothing from aivi. */
|
|
159
|
+
archived: boolean;
|
|
160
|
+
/** Whether the ticket has finished as far as work goes — the adapter's
|
|
161
|
+
* **verdict**, not a platform word: each tracker decides which of its
|
|
162
|
+
* states mean closed (Linear's `completed` and `canceled` both do), and the
|
|
163
|
+
* decision code never sees the words. */
|
|
164
|
+
completed: boolean;
|
|
165
|
+
/** The tickets blocking this one, each with the same verdict: a blocker
|
|
166
|
+
* that has finished holds nothing back. */
|
|
167
|
+
blockedBy: {
|
|
168
|
+
id: string;
|
|
169
|
+
completed: boolean;
|
|
170
|
+
}[];
|
|
171
|
+
}
|
|
172
|
+
/** What changed on a ticket, in neutral words: only the things routing
|
|
173
|
+
* keys on. `archive` is the platform's word for *deleted* — the ticket is
|
|
174
|
+
* gone as work, which stops whatever still works it. Anything else a
|
|
175
|
+
* platform reports is the adapter's to notice and drop. */
|
|
176
|
+
export type TrackerChange = 'state' | 'labels' | 'delegate' | 'archive';
|
|
177
|
+
/** One comment on a ticket, read back by the ticket-desk tools: who wrote
|
|
178
|
+
* it (null when the platform hides the author), the text, and the moment
|
|
179
|
+
* in the platform's own timestamp. */
|
|
180
|
+
export interface TrackerComment {
|
|
181
|
+
author: string | null;
|
|
182
|
+
body: string;
|
|
183
|
+
at: string;
|
|
184
|
+
}
|
|
185
|
+
/** The ticket desk: the effective tool ids of a worker's hand on the ticket
|
|
186
|
+
* itself. Named once here because three packages must speak the same words
|
|
187
|
+
* without one importing another's world: a tracker module **claims** these
|
|
188
|
+
* ids, `aivi setup` seeds a single `aivi_ticket_*` wildcard **deny** (the
|
|
189
|
+
* CLI treats modules as opaque and imports nothing from them), and the
|
|
190
|
+
* agent file that works tickets **allows** them by the same wildcard.
|
|
191
|
+
* Tests hold the claims and the wildcard against this list — the prefix is
|
|
192
|
+
* part of the contract, or the seeded deny stops covering a renamed id. */
|
|
193
|
+
export declare const TICKET_TOOL_ACTIONS: readonly ['aivi_ticket_read', 'aivi_ticket_comments', 'aivi_ticket_labels', 'aivi_ticket_edit', 'aivi_ticket_comment', 'aivi_ticket_create', 'aivi_ticket_add_label', 'aivi_ticket_remove_label'];
|
|
194
|
+
export type TicketToolAction = (typeof TICKET_TOOL_ACTIONS)[number];
|
|
195
|
+
/**
|
|
196
|
+
* The ticket desk in the tracker's own words: what each `aivi_ticket_*` tool
|
|
197
|
+
* does to the ticket its run works. The host owns the tools — one claim of
|
|
198
|
+
* each id, the argument checks, and the guard that the call comes from a live
|
|
199
|
+
* run of this tracker — because that is run domain. What a ticket is on the
|
|
200
|
+
* platform is tracker domain: which conversation a plain comment lands on,
|
|
201
|
+
* which team a new ticket is born in, what "the label the team knows" means.
|
|
202
|
+
* Every method receives the run, whose `ticketId` is the only ticket it may
|
|
203
|
+
* touch; the answer shapes are what the worker reads, and they are the same
|
|
204
|
+
* on every platform.
|
|
205
|
+
*/
|
|
206
|
+
export interface TicketDesk {
|
|
207
|
+
/** Read the ticket: the words a worker starts from, as they now stand. */
|
|
208
|
+
read(run: RunView): Promise<{
|
|
209
|
+
identifier: string;
|
|
210
|
+
title: string;
|
|
211
|
+
description: string | null;
|
|
212
|
+
lane: string;
|
|
213
|
+
labels: string[];
|
|
214
|
+
url: string;
|
|
215
|
+
}>;
|
|
216
|
+
/** The comment trail, oldest first. */
|
|
217
|
+
comments(run: RunView): Promise<TrackerComment[]>;
|
|
218
|
+
/** The team's label catalogue and what stands on the ticket now. */
|
|
219
|
+
labels(run: RunView): Promise<{
|
|
220
|
+
team: string[];
|
|
221
|
+
on: string[];
|
|
222
|
+
}>;
|
|
223
|
+
/** Rewrite the ticket's own words; only what is named is touched. */
|
|
224
|
+
edit(run: RunView, changes: {
|
|
225
|
+
title?: string;
|
|
226
|
+
description?: string;
|
|
227
|
+
}): Promise<{
|
|
228
|
+
identifier: string;
|
|
229
|
+
title: string;
|
|
230
|
+
url: string;
|
|
231
|
+
}>;
|
|
232
|
+
/** A plain comment on the ticket, as aivi's own — what a person typed
|
|
233
|
+
* lands as what a person reads. */
|
|
234
|
+
comment(run: RunView, text: string): Promise<void>;
|
|
235
|
+
/** Open a new ticket — the escape hatch for work this ticket should not
|
|
236
|
+
* carry. Where it lands is the tracker's config, never the caller's word. */
|
|
237
|
+
create(run: RunView, input: {
|
|
238
|
+
title: string;
|
|
239
|
+
description: string;
|
|
240
|
+
}): Promise<{
|
|
241
|
+
identifier: string;
|
|
242
|
+
url: string;
|
|
243
|
+
lane: string;
|
|
244
|
+
}>;
|
|
245
|
+
/** Put one label on the ticket (`on`) or take it off. */
|
|
246
|
+
label(run: RunView, label: string, on: boolean): Promise<void>;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The normalized signals everything downstream keys off — never raw
|
|
250
|
+
* webhooks. `started` is a working session opened on a ticket (a
|
|
251
|
+
* delegation, a mention, whatever brought it); `prompted` is a message
|
|
252
|
+
* inside one, or a signal like a stop; `updated` is a ticket change with
|
|
253
|
+
* `changed` already narrowed to the routing-relevant parts.
|
|
254
|
+
*/
|
|
255
|
+
export type TrackerEvent = {
|
|
256
|
+
kind: 'started';
|
|
257
|
+
conversation: string;
|
|
258
|
+
issueId: string;
|
|
259
|
+
promptContext?: string;
|
|
260
|
+
} | {
|
|
261
|
+
kind: 'prompted';
|
|
262
|
+
id: string;
|
|
263
|
+
conversation: string;
|
|
264
|
+
body?: string;
|
|
265
|
+
signal?: string | null;
|
|
266
|
+
} | {
|
|
267
|
+
kind: 'updated';
|
|
268
|
+
conversation: string;
|
|
269
|
+
issueId: string;
|
|
270
|
+
changed: TrackerChange[];
|
|
271
|
+
};
|
|
272
|
+
/** How a message reads, in platform-neutral words; the adapter picks the
|
|
273
|
+
* platform's own rendering (Linear maps these to agent-session activity
|
|
274
|
+
* types). Adapters keep the distinction honest: a `progress` line is gone
|
|
275
|
+
* when the work ends, an `answer` or `outcome` stays. */
|
|
276
|
+
export type TrackerCommentKind =
|
|
277
|
+
/** A reply to what someone asked — the platform shows it as an answer. */
|
|
278
|
+
'answer'
|
|
279
|
+
/** An ephemeral line while work runs; replaced as work moves. */
|
|
280
|
+
| 'progress'
|
|
281
|
+
/** A durable line about work in flight. */
|
|
282
|
+
| 'note'
|
|
283
|
+
/** A terminal outcome: finished, stopped, or refused. */
|
|
284
|
+
| 'outcome';
|
|
285
|
+
/** What the machinery asks of the platform, in neutral words. The adapter
|
|
286
|
+
* translates and performs; *when* to act stays the machinery's decision. */
|
|
287
|
+
export type TrackerUpdate =
|
|
288
|
+
/** Move the ticket to the named lane. */
|
|
289
|
+
{
|
|
290
|
+
kind: 'move';
|
|
291
|
+
lane: string;
|
|
292
|
+
}
|
|
293
|
+
/** Add or remove one label. */
|
|
294
|
+
| {
|
|
295
|
+
kind: 'label';
|
|
296
|
+
label: string;
|
|
297
|
+
on: boolean;
|
|
298
|
+
}
|
|
299
|
+
/** Leave a visible comment on the ticket itself. */
|
|
300
|
+
| {
|
|
301
|
+
kind: 'comment';
|
|
302
|
+
text: string;
|
|
303
|
+
};
|
|
304
|
+
/** One moment of a worker's progress, in neutral words: the human-readable
|
|
305
|
+
* status line as already rendered, and the tool the worker is running when
|
|
306
|
+
* there is one. Rendered by the machinery (the same reducer a chat progress
|
|
307
|
+
* placeholder reads), delivered the platform's way. */
|
|
308
|
+
export interface TrackerProgressLine {
|
|
309
|
+
/** The status as text: "🔧 reading src/x.ts", "⏳ thinking…". */
|
|
310
|
+
text: string;
|
|
311
|
+
/** The running tool, when there is one and the platform shows tools. */
|
|
312
|
+
tool?: {
|
|
313
|
+
name: string;
|
|
314
|
+
detail?: string;
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The tracker's **platform**: the ticket system behind a tracker module —
|
|
319
|
+
* Linear, Jira, GitHub Issues — seen as the conversation-keyed translator it
|
|
320
|
+
* is. A tracker owns tickets — lanes, delegation, what people write on them;
|
|
321
|
+
* the platform speaks its platform's voice: it turns the platform's events
|
|
322
|
+
* into the neutral ones above, aivi's neutral updates into the platform's
|
|
323
|
+
* mutations, and it is **required** to render a worker's question and a run's
|
|
324
|
+
* outcome the way its platform shows them (`ask`, and the answer/outcome
|
|
325
|
+
* kinds of `comment`) — the host's orchestrator owns the run itself: its
|
|
326
|
+
* durable record, its session, the two worker tools and the rule that only a
|
|
327
|
+
* tool call ends it. Channels use platforms, forges use platforms, and this
|
|
328
|
+
* is the same word for the same thing: the system aivi talks to over an API.
|
|
329
|
+
*
|
|
330
|
+
* The machinery consuming this ships in the host. The exact member set is
|
|
331
|
+
* **provisional by design**: roughly seven trackers will move it, and every
|
|
332
|
+
* entry earns its place with a second tracker's need.
|
|
333
|
+
*/
|
|
334
|
+
export interface Platform {
|
|
335
|
+
/** The module id this tracker speaks for (`tracker-linear`). */
|
|
336
|
+
readonly id: string;
|
|
337
|
+
/** The team's workflow states as the platform holds them, so a proposed
|
|
338
|
+
* lane mapping can be validated against what exists. */
|
|
339
|
+
laneStates(conversation: string, teamId: string): Promise<TrackerState[]>;
|
|
340
|
+
/** Create the named states the mapping needs and none platform policy
|
|
341
|
+
* refuses, returning the ones made. Opt-in with an explicit yes per
|
|
342
|
+
* team, never silent. Absent means the tracker cannot create states. */
|
|
343
|
+
createStates?(conversation: string, teamId: string, missing: string[]): Promise<TrackerState[]>;
|
|
344
|
+
/** Ready issues in a lane, in the platform's own order, each with the
|
|
345
|
+
* facts needed to build a worker's first message. Absent until a
|
|
346
|
+
* pull-shaped tracker needs it. */
|
|
347
|
+
candidates?(conversation: string, lane: string): Promise<TrackerIssue[]>;
|
|
348
|
+
/** Apply one neutral outcome to a ticket (a lane move, one label, a plain
|
|
349
|
+
* comment). Required: the machinery's asks all speak through it. */
|
|
350
|
+
apply(conversation: string, issueId: string, update: TrackerUpdate): Promise<void>;
|
|
351
|
+
/** The ticket, fresh from the platform — the only routing input, never
|
|
352
|
+
* trusted from an event's own words. Delivered in the neutral shape;
|
|
353
|
+
* the adapter's translation is where platform fields stop existing. */
|
|
354
|
+
issue(conversation: string, issueId: string): Promise<TrackerIssue>;
|
|
355
|
+
/** Make (with `null`: unmake) aivi the ticket's delegate — the neutral
|
|
356
|
+
* word for whoever the platform names to work it. */
|
|
357
|
+
assign(conversation: string, issueId: string, userId: string): Promise<void>;
|
|
358
|
+
unassign(conversation: string, issueId: string): Promise<void>;
|
|
359
|
+
/** Open a working session on the ticket under aivi's own identity,
|
|
360
|
+
* returning its conversation id; null when the platform made none.
|
|
361
|
+
* Some platforms create the session as a side effect of `assign`;
|
|
362
|
+
* hiding that is exactly the adapter's job. */
|
|
363
|
+
startSession(conversation: string, issueId: string): Promise<string | null>;
|
|
364
|
+
/** The conversation id for a session the platform names, in the
|
|
365
|
+
* adapter's namespacing; what the store binds. */
|
|
366
|
+
idFor(sessionId: string): string;
|
|
367
|
+
/** The other direction, for what is named *from* a conversation: the
|
|
368
|
+
* configured app within aivi that speaks it, and the platform's own
|
|
369
|
+
* session id inside it (a worker's worktree is named from that). The
|
|
370
|
+
* conversation string itself is the adapter's business, never parsed
|
|
371
|
+
* by the machinery. */
|
|
372
|
+
parts(conversation: string): {
|
|
373
|
+
app: string;
|
|
374
|
+
session: string;
|
|
375
|
+
};
|
|
376
|
+
/** The workspace (organization) the conversation's app speaks in; what
|
|
377
|
+
* routes a ticket to a project besides the team. */
|
|
378
|
+
orgOf(conversation: string): Promise<string>;
|
|
379
|
+
/** Whose user id aivi is in the conversation's workspace — the delegate
|
|
380
|
+
* id `assign` and `startSession` use, and what a "delegate changed"
|
|
381
|
+
* check compares against. */
|
|
382
|
+
ownerOf(conversation: string): string;
|
|
383
|
+
/** One message into the conversation, rendered the platform's way; the
|
|
384
|
+
* platform's message id when it names them (the engine may edit by it). */
|
|
385
|
+
comment(conversation: string, text: string, kind: TrackerCommentKind): Promise<string | undefined>;
|
|
386
|
+
/**
|
|
387
|
+
* Show what a worker is doing **right now**, replacing whatever was shown
|
|
388
|
+
* before (ruled 2026-10-02: thoughts and actions, ephemeral — a working
|
|
389
|
+
* run says what it does without ever spamming the ticket). **Transient by
|
|
390
|
+
* contract**: a replaced line must not linger as a permanent record.
|
|
391
|
+
* Linear posts an ephemeral `action` activity when a tool runs and an
|
|
392
|
+
* ephemeral `thought` otherwise. Absent means the platform has no
|
|
393
|
+
* progress surface and hears nothing: a worker there works in silence,
|
|
394
|
+
* and the follower never calls it.
|
|
395
|
+
*/
|
|
396
|
+
progress?(conversation: string, line: TrackerProgressLine): Promise<void>;
|
|
397
|
+
/**
|
|
398
|
+
* A plain notice on the ticket **itself**, for a run that died before it
|
|
399
|
+
* had a conversation to speak through (ruled 2026-10-02 after seven silent
|
|
400
|
+
* early failures): silence is not an answer, and the ticket is the only
|
|
401
|
+
* board a person reads. The `conversation` is the app's own feed — there
|
|
402
|
+
* is no session yet.
|
|
403
|
+
*/
|
|
404
|
+
notify(conversation: string, issueId: string, text: string): Promise<void>;
|
|
405
|
+
/** Whether this working session has already had its result rendered —
|
|
406
|
+
* the follower's idempotence question, decided the platform's own way
|
|
407
|
+
* (Linear: the agent session is ended; another: a result comment exists).
|
|
408
|
+
* A catch-up that would say it twice says it once. */
|
|
409
|
+
resultShown(conversation: string): Promise<boolean>;
|
|
410
|
+
/** A closing note on the **ticket itself**, so an ending is readable
|
|
411
|
+
* without opening the agent session (ruled 2026-10-02: the answer was
|
|
412
|
+
* only visible inside the session — "hard to get to now"). Optional: a
|
|
413
|
+
* platform with nothing standing outside the session posts no extra note
|
|
414
|
+
* and loses nothing. **Idempotence is the adapter's** — the follower
|
|
415
|
+
* retries this on every wake and boot like the rest of the ceremony, so
|
|
416
|
+
* a note already standing must never be posted twice. */
|
|
417
|
+
closingNote?(conversation: string, issueId: string, text: string): Promise<void>;
|
|
418
|
+
/**
|
|
419
|
+
* Put a worker's question to the people, in the platform's own shape —
|
|
420
|
+
* the same `RunQuestion` the orchestrator recorded, rendered here.
|
|
421
|
+
* **Required of every tracker** (ruled 2026-09-30): the orchestrator
|
|
422
|
+
* records the question and does nothing else with it — only the adapter
|
|
423
|
+
* knows what a question looks like on its platform (Linear renders an
|
|
424
|
+
* elicitation activity, with its `select` signal when there are options;
|
|
425
|
+
* GitHub Issues would post a numbered comment). The answer arrives as an
|
|
426
|
+
* ordinary message event; the open OpenCode form is the discriminator,
|
|
427
|
+
* read from OpenCode, never a guess from texts.
|
|
428
|
+
*/
|
|
429
|
+
ask(conversation: string, question: RunQuestion): Promise<void>;
|
|
430
|
+
/**
|
|
431
|
+
* Show the worker's working plan — the whole checklist, as it now stands.
|
|
432
|
+
* A forwarding like the orchestrator's own: the tracker renders it in its
|
|
433
|
+
* platform's shape (Linear replaces the session's agent plan wholesale)
|
|
434
|
+
* and nothing about a run waits on its delivery.
|
|
435
|
+
*/
|
|
436
|
+
plan(conversation: string, plan: RunPlan): Promise<void>;
|
|
437
|
+
/**
|
|
438
|
+
* The ticket desk — what `aivi_ticket_*` tools do to tickets **directly**,
|
|
439
|
+
* with no agent session to speak through (the `conversation` is the app's
|
|
440
|
+
* own feed, as in `notify`). **Required as a set** (ruled 2026-10-07): the
|
|
441
|
+
* host claims the desk ids once for the whole home and routes each call to
|
|
442
|
+
* the tracker that works the run's ticket, so a tracker that skipped its
|
|
443
|
+
* desk would fail its own workers rather than nobody's. All of it is the
|
|
444
|
+
* **worker's hand on the ticket**, gated by OpenCode permissions (denied by
|
|
445
|
+
* default, allowed per agent); nothing here decides who may call.
|
|
446
|
+
*/
|
|
447
|
+
ticketComments(conversation: string, issueId: string): Promise<TrackerComment[]>;
|
|
448
|
+
/** A plain comment on the ticket, as the app's own — no activity type,
|
|
449
|
+
* no marker: what a person typed lands as what a person reads. */
|
|
450
|
+
addComment(conversation: string, issueId: string, text: string): Promise<void>;
|
|
451
|
+
/** Rewrite the ticket's own words: title and/or description. Returns the
|
|
452
|
+
* ticket as it now stands, so the caller reports the truth it wrote. */
|
|
453
|
+
editIssue(conversation: string, issueId: string, changes: {
|
|
454
|
+
title?: string;
|
|
455
|
+
description?: string;
|
|
456
|
+
}): Promise<TrackerIssue>;
|
|
457
|
+
/** Open a new ticket in the named team, landing it in `lane` when a lane
|
|
458
|
+
* is named (the adapter resolves the name against the team's states and
|
|
459
|
+
* says so when none matches). */
|
|
460
|
+
createIssue(conversation: string, input: {
|
|
461
|
+
teamId: string;
|
|
462
|
+
title: string;
|
|
463
|
+
description?: string;
|
|
464
|
+
lane?: string;
|
|
465
|
+
}): Promise<TrackerIssue>;
|
|
466
|
+
/** The team's label catalogue — every name the team knows, so a worker
|
|
467
|
+
* can pick honestly instead of guessing. */
|
|
468
|
+
teamLabels(conversation: string, teamId: string): Promise<string[]>;
|
|
469
|
+
/** Subscribe to the normalized events. The adapter owns the platform's
|
|
470
|
+
* whole inbound surface — endpoints, signatures, acknowledgements;
|
|
471
|
+
* registration happens at module start. Returns the unsubscribe. */
|
|
472
|
+
events(sink: (event: TrackerEvent) => Promise<void>): () => void;
|
|
473
|
+
}
|
package/dist/tracker.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **tracker** contract, in the two seams a tracker module lives between.
|
|
3
|
+
*
|
|
4
|
+
* `Tracker` is the seam between the host's orchestrator and the module: the
|
|
5
|
+
* **stages** a run is walked through, keyed by the run. The orchestrator
|
|
6
|
+
* walks them in order and awaits only the lifecycle ones; it never learns
|
|
7
|
+
* what a ticket platform is. `Platform` is the seam between the module and
|
|
8
|
+
* its ticket platform: the conversation-keyed translator that turns the
|
|
9
|
+
* platform's events into neutral ones and aivi's neutral updates into the
|
|
10
|
+
* platform's mutations. A module registers one `Tracker` with the
|
|
11
|
+
* orchestrator and speaks its `Platform` to whomever on its platform; the
|
|
12
|
+
* stages that render are usually one-line forwards — the division is honest,
|
|
13
|
+
* not duplicated.
|
|
14
|
+
*
|
|
15
|
+
* The words here are platform-neutral by design: roughly seven trackers will
|
|
16
|
+
* move this contract, and every entry earns its place with a second
|
|
17
|
+
* tracker's need. The run shapes the stages carry are the shared run domain
|
|
18
|
+
* (`./run.ts`); the ticket shapes here are what the tracker reports and the
|
|
19
|
+
* orchestrator routes.
|
|
20
|
+
*/
|
|
21
|
+
/** The ticket desk: the effective tool ids of a worker's hand on the ticket
|
|
22
|
+
* itself. Named once here because three packages must speak the same words
|
|
23
|
+
* without one importing another's world: a tracker module **claims** these
|
|
24
|
+
* ids, `aivi setup` seeds a single `aivi_ticket_*` wildcard **deny** (the
|
|
25
|
+
* CLI treats modules as opaque and imports nothing from them), and the
|
|
26
|
+
* agent file that works tickets **allows** them by the same wildcard.
|
|
27
|
+
* Tests hold the claims and the wildcard against this list — the prefix is
|
|
28
|
+
* part of the contract, or the seeded deny stops covering a renamed id. */
|
|
29
|
+
export const TICKET_TOOL_ACTIONS = [
|
|
30
|
+
'aivi_ticket_read',
|
|
31
|
+
'aivi_ticket_comments',
|
|
32
|
+
'aivi_ticket_labels',
|
|
33
|
+
'aivi_ticket_edit',
|
|
34
|
+
'aivi_ticket_comment',
|
|
35
|
+
'aivi_ticket_create',
|
|
36
|
+
'aivi_ticket_add_label',
|
|
37
|
+
'aivi_ticket_remove_label',
|
|
38
|
+
];
|
package/dist/voice.d.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The voice contract: hearing as a capability. The voice plugin provides the
|
|
3
|
+
* one adapter (models, diarizer, person memory, wake, transcripts); modules
|
|
4
|
+
* that stand in a room — discord today, meet/teams/zoom tomorrow — open a
|
|
5
|
+
* `VoiceRoom` and feed it audio. When no adapter was provided, opening a
|
|
6
|
+
* room fails with `VoiceNotLoaded`: the plugin is optional, the failure is
|
|
7
|
+
* honest, and no ordering ceremony hides which module is missing.
|
|
8
|
+
*
|
|
9
|
+
* Who the speakers are is the room's business, not the contract's: a
|
|
10
|
+
* platform that names its speakers (Discord) says so per utterance; a room
|
|
11
|
+
* that only has mixed audio (a browser tab, a meeting bridge) pushes raw
|
|
12
|
+
* PCM and the adapter does ASR, diarization and person identification from
|
|
13
|
+
* the audio alone — when a diarizer is configured. Without one the cuts
|
|
14
|
+
* come from loudness and the lines carry no name: a guessed name would
|
|
15
|
+
* invent people (ruled 2026-10-14).
|
|
16
|
+
*/
|
|
17
|
+
/** No voice plugin, no hearing. The fix belongs in the message. */
|
|
18
|
+
export declare class VoiceNotLoaded extends Error {
|
|
19
|
+
constructor();
|
|
20
|
+
}
|
|
21
|
+
export interface RoomOptions {
|
|
22
|
+
/** Platform the room is on (`discord`, `meet`): the origin on
|
|
23
|
+
* everything it produces, never the package that speaks for it. */
|
|
24
|
+
id: string;
|
|
25
|
+
/** What this room is, for the transcript's source line. */
|
|
26
|
+
label: string;
|
|
27
|
+
/** True when the room will push raw mixed audio through `feed` and let
|
|
28
|
+
* voice do everything — segmentation, speaker separation, person
|
|
29
|
+
* identification. Rooms whose platform already demuxes named speakers
|
|
30
|
+
* (Discord, through `lane()`) leave it out: mixed events are another
|
|
31
|
+
* room's business. */
|
|
32
|
+
streaming?: boolean;
|
|
33
|
+
/** What this room IS: `call` (default — the conversation, not a record:
|
|
34
|
+
* closed speech prompts aivi, barge-in lives here) or `meeting` (lines
|
|
35
|
+
* append; the wake phrase activates, nothing else does). The module that
|
|
36
|
+
* opens the room declares it (ruled 2026-10-13: mode belongs to the
|
|
37
|
+
* room, not to the voice block; call is the default because the plain
|
|
38
|
+
* voice system for a normal session is the common case — and loads the
|
|
39
|
+
* least). */
|
|
40
|
+
mode?: 'call' | 'meeting';
|
|
41
|
+
/** The quiet after which the room's next exchange gets a FRESH OpenCode
|
|
42
|
+
* session instead of the resumed one (0: fresh every join; false: the
|
|
43
|
+
* same session forever). Absent: the voice block's `resumeMinutes` —
|
|
44
|
+
* the global window. A meeting room may want a shorter memory than a
|
|
45
|
+
* Discord channel (a call is a call: the next one starts clean). */
|
|
46
|
+
resumeMinutes?: number | false;
|
|
47
|
+
/** Dispatcher pool this room's sessions draw capacity from — a name in
|
|
48
|
+
* `dispatcher.pools`. Absent: the voice block's `pool`. Openers keep
|
|
49
|
+
* their categories apart this way: meeting rooms on a big local model,
|
|
50
|
+
* calls on a snappy one, each with its own seats (operator, 2026-10-15:
|
|
51
|
+
* voice capacity is per category, like LLM endpoints). */
|
|
52
|
+
pool?: string;
|
|
53
|
+
/** The OpenCode agent the room's sessions run as. The opener names it —
|
|
54
|
+
* a channel's own agent is the natural filling (a Discord room speaks as
|
|
55
|
+
* the Discord agent unless told otherwise); absent: `assistant`. */
|
|
56
|
+
agent?: string;
|
|
57
|
+
}
|
|
58
|
+
/** One finished piece of speech. `speaker` is the platform's own answer —
|
|
59
|
+
* when it has none, leave it out and the adapter identifies from audio. */
|
|
60
|
+
export interface Utterance {
|
|
61
|
+
speaker?: string;
|
|
62
|
+
/** Platform user id, kept with the line when the platform has one. */
|
|
63
|
+
user?: string;
|
|
64
|
+
/** Platform channel/room id, kept with the line when it varies per line. */
|
|
65
|
+
channel?: string;
|
|
66
|
+
/** This speaker is a known person of aivi (a linked account), not a bot or
|
|
67
|
+
* a stranger: only their words may interrupt a live answer (barge-in). */
|
|
68
|
+
person?: boolean;
|
|
69
|
+
/** 16 kHz mono float PCM. */
|
|
70
|
+
pcm: Float32Array;
|
|
71
|
+
}
|
|
72
|
+
/** A named speaker's LIVE lane: their audio streams in while they speak and
|
|
73
|
+
* words come back out as they are heard — the platform (Discord) already
|
|
74
|
+
* demuxes speakers, so the adapter streams ASR without diarizing. One lane
|
|
75
|
+
* is one utterance. */
|
|
76
|
+
export interface VoiceLane {
|
|
77
|
+
/** Live 16 kHz mono float PCM, in real time, as the platform's packets land. */
|
|
78
|
+
push(pcm: Float32Array): void;
|
|
79
|
+
/** The utterance is over (the platform's own silence rule closed it):
|
|
80
|
+
* flush the tail, then the line is committed with the streamed text —
|
|
81
|
+
* or, if no live lane ever came up, batch-transcribed from the buffered
|
|
82
|
+
* audio. Resolves with the text, or null when the utterance was lost. */
|
|
83
|
+
end(): Promise<string | null>;
|
|
84
|
+
/** Throw the utterance away (too short to be speech): no line, the live
|
|
85
|
+
* lane is closed unheard. */
|
|
86
|
+
abort(): void;
|
|
87
|
+
}
|
|
88
|
+
/** A handle on one room. Audio goes in; lines, events and aivi's spoken
|
|
89
|
+
* replies come out. Everything the handle does that needs a model either
|
|
90
|
+
* works or says so in the log — a lost utterance is a logged loss. */
|
|
91
|
+
export interface VoiceRoom {
|
|
92
|
+
/** Streaming room audio: s16le 16 kHz mono chunks, real time, anonymous.
|
|
93
|
+
* The adapter streams ASR, identifies, scans for the wake phrase and
|
|
94
|
+
* appends transcript lines. Diarizes only in meeting mode — call mode
|
|
95
|
+
* reads voice activity off the waveform. Fire-and-forget by nature. */
|
|
96
|
+
feed(pcm: Buffer): void;
|
|
97
|
+
/** Open a live lane for one named speaker's utterance. In a call room the
|
|
98
|
+
* first recognized WORD from a linked person is the interruption
|
|
99
|
+
* (barge-in) — not the speaking start, which carries no content. */
|
|
100
|
+
lane(init: {
|
|
101
|
+
speaker: string;
|
|
102
|
+
user: string;
|
|
103
|
+
person?: boolean;
|
|
104
|
+
channel?: string;
|
|
105
|
+
}): VoiceLane;
|
|
106
|
+
/** A finished utterance a platform already segmented and named (platforms
|
|
107
|
+
* that get a live stream per speaker use `lane()` instead): transcribe
|
|
108
|
+
* it, append the line under that speaker. Resolves with the text, or null
|
|
109
|
+
* when the utterance was lost. */
|
|
110
|
+
heard(utt: Utterance): Promise<string | null>;
|
|
111
|
+
/** Be the room's mouth: called with a wav whenever aivi's answer should be
|
|
112
|
+
* spoken there (the room's microphone *is* this audio) — once per audio
|
|
113
|
+
* chunk, in order; a sentence spoken while it synthesizes lands as
|
|
114
|
+
* several wavs. Return false when nothing played. A mouth that queues
|
|
115
|
+
* the audio returns a promise that settles when THIS wav is done with —
|
|
116
|
+
* played, dropped by a barge, or failed: the room holds the answer's
|
|
117
|
+
* speaking window open until every chunk settles, and interrupting words
|
|
118
|
+
* are verified against that window. Resolving at enqueue means aivi
|
|
119
|
+
* talks through the interruption unheard. `stop` (optional) silences the
|
|
120
|
+
* mouth NOW — barge-in calls it: the playing answer stops mid-sentence,
|
|
121
|
+
* nothing new starts (and every queued promise settles). `flush`
|
|
122
|
+
* (optional) says the answer is over — the room calls it once the last
|
|
123
|
+
* chunk has been handed over (never on the barge path, where `stop`
|
|
124
|
+
* already closed everything): a mouth playing one continuous stream per
|
|
125
|
+
* answer ends its stream here so the player drains back to Idle. Returns
|
|
126
|
+
* the unregister. */
|
|
127
|
+
mouth(play: (wav: Buffer) => boolean | Promise<boolean>, stop?: () => void, flush?: () => void): () => void;
|
|
128
|
+
/** Stop talking and listen (the conversation interrupting): silence the
|
|
129
|
+
* playing answer and drop the answers still queued to be spoken. The
|
|
130
|
+
* interrupting speech arrives as speech and is heard like any other —
|
|
131
|
+
* call rooms put it to the session, which gets to reconsider. Meeting
|
|
132
|
+
* rooms are polite: this does nothing there. */
|
|
133
|
+
bargeIn(): void;
|
|
134
|
+
/** Watch what the room produces for display: fusion events
|
|
135
|
+
* (`{ev:"delta"…}`, `{ev:"line"…}`, `{ev:"session_reset"…}`). */
|
|
136
|
+
onEvent(listener: (event: Record<string, unknown>) => void): () => void;
|
|
137
|
+
stop(): Promise<void>;
|
|
138
|
+
}
|
|
139
|
+
/** What the voice plugin provides at start — exactly one, host-wide. */
|
|
140
|
+
export interface VoiceAdapter {
|
|
141
|
+
room(options: RoomOptions): VoiceRoom;
|
|
142
|
+
}
|
|
143
|
+
/** The service on `AiviServices`: the provider speaks once, every room
|
|
144
|
+
* holder asks the same question — is there an adapter, and if so open one. */
|
|
145
|
+
export interface Voice {
|
|
146
|
+
provide(adapter: VoiceAdapter): () => void;
|
|
147
|
+
room(options: RoomOptions): VoiceRoom;
|
|
148
|
+
}
|