@aivi/tracker-github 0.0.0-stage → 0.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.
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +34 -0
- package/dist/client.d.ts +247 -0
- package/dist/client.js +528 -0
- package/dist/config.d.ts +60 -0
- package/dist/config.js +99 -0
- package/dist/desk.d.ts +13 -0
- package/dist/desk.js +67 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +10 -0
- package/dist/links.d.ts +56 -0
- package/dist/links.js +94 -0
- package/dist/module.d.ts +36 -0
- package/dist/module.js +817 -0
- package/dist/probe.d.ts +104 -0
- package/dist/probe.js +312 -0
- package/dist/projects.d.ts +41 -0
- package/dist/projects.js +117 -0
- package/dist/runprogress.d.ts +62 -0
- package/dist/runprogress.js +122 -0
- package/dist/setup-project.d.ts +125 -0
- package/dist/setup-project.js +638 -0
- package/dist/setup.d.ts +3 -0
- package/dist/setup.js +262 -0
- package/dist/tracker.d.ts +174 -0
- package/dist/tracker.js +505 -0
- package/dist/webhook.d.ts +102 -0
- package/dist/webhook.js +52 -0
- package/dist/work.d.ts +60 -0
- package/dist/work.js +83 -0
- package/docs/github-tracker.md +122 -0
- package/package.json +59 -4
- package/README.md +0 -3
package/dist/work.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Logger } from '@aivi/core';
|
|
2
|
+
import type { AiviServices } from '@aivi/plugin';
|
|
3
|
+
import type { GithubClient } from './client.ts';
|
|
4
|
+
import type { GithubConfig } from './config.ts';
|
|
5
|
+
/** The ticket's own words: what it is, what it says. This is the **summary**
|
|
6
|
+
* `initWork` hands the orchestrator, and the orchestrator composes the
|
|
7
|
+
* worker's first prompt around it — the same dossier for a queue pickup
|
|
8
|
+
* and a plain walk claim: the ticket's text does not depend on how the
|
|
9
|
+
* worker was reached. */
|
|
10
|
+
export declare const issueDossier: (issue: {
|
|
11
|
+
identifier: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string | null;
|
|
14
|
+
}) => string;
|
|
15
|
+
/** What the walk reads and what `initWork` opens through: the GitHub board,
|
|
16
|
+
* in the installation's own credentials. */
|
|
17
|
+
export interface GithubBoard {
|
|
18
|
+
projects(): string[];
|
|
19
|
+
tickets(projectId: string, lane: string): Promise<{
|
|
20
|
+
id: string;
|
|
21
|
+
blocked: boolean;
|
|
22
|
+
}[]>;
|
|
23
|
+
moveTo(projectId: string, ticketId: string, lane: string): Promise<void>;
|
|
24
|
+
/** The ticket fresh from the board, or undefined when a person deleted it. */
|
|
25
|
+
issue(ticketId: string): Promise<GithubIssueWords | undefined>;
|
|
26
|
+
}
|
|
27
|
+
/** The fields of a listed issue the tracker's stages speak with. */
|
|
28
|
+
export interface GithubIssueWords {
|
|
29
|
+
identifier: string;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string | null;
|
|
32
|
+
/** The repository — GitHub's team. */
|
|
33
|
+
teamId: string;
|
|
34
|
+
/** The ticket's Status on the board its repository feeds; '' when the
|
|
35
|
+
* repository feeds no project. */
|
|
36
|
+
stateName: string;
|
|
37
|
+
/** GitHub names no branch; the tracker generates one (D8). */
|
|
38
|
+
branchName: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The GitHub board as the orchestrator's eligibility walk reads it
|
|
42
|
+
* (docs/orchestrator.md, "How work gets picked"): which projects this
|
|
43
|
+
* installation speaks for, what sits in a lane, and how a ticket enters one.
|
|
44
|
+
*
|
|
45
|
+
* A GitHub repository **is** the team: a project maps its repositories to one
|
|
46
|
+
* projectsV2 board (D1 — one tracker per project, and the walk visits the
|
|
47
|
+
* project's lanes as the board's Status options). The walk's order is each
|
|
48
|
+
* repository's open issues, newest created first — GitHub's own list order;
|
|
49
|
+
* the board's per-view ordering is a person's reading and no eligibility
|
|
50
|
+
* depends on it.
|
|
51
|
+
*
|
|
52
|
+
* What comes back **is** eligible work: closed tickets are out of the listing
|
|
53
|
+
* entirely, and out here go the tickets carrying the human-work label and the
|
|
54
|
+
* tickets with any `Agent` value — someone already speaks for them, aivi
|
|
55
|
+
* included (a hand-set `Agent` gets the fixed refusal in the module, and the
|
|
56
|
+
* walk starts nothing twice). A stop is not remembered here (ruled
|
|
57
|
+
* 2026-10-02): a stopped ticket goes back to the board, and "not that one
|
|
58
|
+
* again" is the human label's job, not a memory in somebody's database.
|
|
59
|
+
*/
|
|
60
|
+
export declare function githubBoard(config: GithubConfig, services: Pick<AiviServices, 'loaded'>, client: GithubClient, log: Logger): GithubBoard;
|
package/dist/work.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { projectForIssue, projectGithub } from './projects.js';
|
|
2
|
+
/** The ticket's own words: what it is, what it says. This is the **summary**
|
|
3
|
+
* `initWork` hands the orchestrator, and the orchestrator composes the
|
|
4
|
+
* worker's first prompt around it — the same dossier for a queue pickup
|
|
5
|
+
* and a plain walk claim: the ticket's text does not depend on how the
|
|
6
|
+
* worker was reached. */
|
|
7
|
+
export const issueDossier = (issue) => `<issue identifier="${issue.identifier}"><title>${issue.title}</title><description>${issue.description ?? ''}</description></issue>`;
|
|
8
|
+
/**
|
|
9
|
+
* The GitHub board as the orchestrator's eligibility walk reads it
|
|
10
|
+
* (docs/orchestrator.md, "How work gets picked"): which projects this
|
|
11
|
+
* installation speaks for, what sits in a lane, and how a ticket enters one.
|
|
12
|
+
*
|
|
13
|
+
* A GitHub repository **is** the team: a project maps its repositories to one
|
|
14
|
+
* projectsV2 board (D1 — one tracker per project, and the walk visits the
|
|
15
|
+
* project's lanes as the board's Status options). The walk's order is each
|
|
16
|
+
* repository's open issues, newest created first — GitHub's own list order;
|
|
17
|
+
* the board's per-view ordering is a person's reading and no eligibility
|
|
18
|
+
* depends on it.
|
|
19
|
+
*
|
|
20
|
+
* What comes back **is** eligible work: closed tickets are out of the listing
|
|
21
|
+
* entirely, and out here go the tickets carrying the human-work label and the
|
|
22
|
+
* tickets with any `Agent` value — someone already speaks for them, aivi
|
|
23
|
+
* included (a hand-set `Agent` gets the fixed refusal in the module, and the
|
|
24
|
+
* walk starts nothing twice). A stop is not remembered here (ruled
|
|
25
|
+
* 2026-10-02): a stopped ticket goes back to the board, and "not that one
|
|
26
|
+
* again" is the human label's job, not a memory in somebody's database.
|
|
27
|
+
*/
|
|
28
|
+
export function githubBoard(config, services, client, log) {
|
|
29
|
+
/** A ticket the walk may start: not a person's to work, and nobody already
|
|
30
|
+
* speaks for it — not eligible, and never knocked twice. */
|
|
31
|
+
const walkable = (issue) => !issue.completed && !issue.labels.some(l => l.name === config.humanLabel) && !issue.agent;
|
|
32
|
+
return {
|
|
33
|
+
projects: () => services.loaded.projects.filter(p => !p.removed && projectGithub(services.loaded, p.id)).map(p => p.id),
|
|
34
|
+
tickets: async (projectId, lane) => {
|
|
35
|
+
const github = projectGithub(services.loaded, projectId);
|
|
36
|
+
if (!github)
|
|
37
|
+
return [];
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const repo of github.repos) {
|
|
40
|
+
const read = await client.openIssues(repo);
|
|
41
|
+
if (read.truncated)
|
|
42
|
+
log.warn('walk.board.truncated', { project: projectId, lane, repo });
|
|
43
|
+
for (const issue of read.issues) {
|
|
44
|
+
const standing = client.laneOf(issue, github.board);
|
|
45
|
+
if (!standing || standing.name !== lane)
|
|
46
|
+
continue; // not on this board, not in this lane
|
|
47
|
+
if (!walkable({
|
|
48
|
+
completed: issue.state === 'closed',
|
|
49
|
+
labels: issue.labels,
|
|
50
|
+
agent: client.agentOn(issue, github.board),
|
|
51
|
+
}))
|
|
52
|
+
continue;
|
|
53
|
+
out.push({ id: issue.ref, blocked: issue.blockedBy.some(b => !b.completed) });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
},
|
|
58
|
+
moveTo: async (projectId, ticketId, lane) => {
|
|
59
|
+
const github = projectGithub(services.loaded, projectId);
|
|
60
|
+
if (!github)
|
|
61
|
+
throw new Error(`project ${projectId} has no GitHub board: ${ticketId} cannot enter lane "${lane}"`);
|
|
62
|
+
// Idempotence is the board's promise: the orchestrator re-drives an
|
|
63
|
+
// owed move after a retry and from the next boot, and a move that
|
|
64
|
+
// already landed lands here as nothing — no second word to GitHub.
|
|
65
|
+
// (The client holds that check: onto the board first, then the option,
|
|
66
|
+
// and an option it lacks is said by name, never created.)
|
|
67
|
+
await client.setLane(ticketId, github.board, lane);
|
|
68
|
+
},
|
|
69
|
+
issue: async (ticketId) => {
|
|
70
|
+
const issue = await client.issue(ticketId);
|
|
71
|
+
const github = projectForIssue(services.loaded, { repo: issue.repo })?.github;
|
|
72
|
+
const lane = github ? client.laneOf(issue, github.board) : undefined;
|
|
73
|
+
return {
|
|
74
|
+
identifier: issue.ref,
|
|
75
|
+
title: issue.title,
|
|
76
|
+
description: issue.body,
|
|
77
|
+
teamId: issue.repo,
|
|
78
|
+
stateName: lane?.name ?? '',
|
|
79
|
+
branchName: '',
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# GitHub — the tracker's configuration
|
|
2
|
+
|
|
3
|
+
The `plugins.tracker-github` block is the GitHub tracker's whole setup; the
|
|
4
|
+
module runs when `@aivi/tracker-github` stands in the `aivi-plugins` list. The
|
|
5
|
+
key is the package name; the database's prefix, the conversation ids
|
|
6
|
+
(`github:…`), the webhook route `/github/webhooks` and the `aivi github`
|
|
7
|
+
command keep the platform's short name, since those say who a conversation is
|
|
8
|
+
on rather than which package speaks for it.
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"plugins": {
|
|
13
|
+
"tracker-github": {
|
|
14
|
+
"agent": "assistant",
|
|
15
|
+
"humanLabel": "needs-human",
|
|
16
|
+
"pool": "default",
|
|
17
|
+
"progress": "tools",
|
|
18
|
+
"trust": "members"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Field | Meaning |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `agent` | The OpenCode agent that answers people on GitHub — comment mentions no run is working: the **assistant**. A hand delegation is never its to answer: those get the fixed refusal. Default: `assistant` |
|
|
27
|
+
| `humanLabel` | Issues carrying this label are never worked automatically; a failed closing and a hand-delegation refusal mark with it. Unlike Linear, where it must pre-exist, `aivi projects add` **creates** it in every mapped repository |
|
|
28
|
+
| `pool` | Dispatcher pool an assistant turn takes a slot in (must exist in `dispatcher.pools` when pools are configured) |
|
|
29
|
+
| `progress` | `silent`, `status` or `tools`: what the status comment shows while a worker runs |
|
|
30
|
+
| `trust` | Who comments may reach aivi — form answers, run steering, the assistant. `members`: GitHub's own `author_association` of OWNER, MEMBER or COLLABORATOR only; `everyone`: any GitHub user. A read per delivery, so a promotion or a removal lands at once. Default: `members` |
|
|
31
|
+
|
|
32
|
+
On a public repository anyone may comment. The trust line sits on the
|
|
33
|
+
comment event alone — one gate for all three paths — and a comment below it
|
|
34
|
+
is heard as nothing: no reply (silence does not advertise the bot), no log
|
|
35
|
+
line (drive-by comments are a public repository's weather). The other two
|
|
36
|
+
events need no gate: a lane or label move needs write access GitHub itself
|
|
37
|
+
enforces.
|
|
38
|
+
|
|
39
|
+
The project entry names its **board** and the **repositories** that hold
|
|
40
|
+
its issues. The board's own deliveries name the board and route by its id;
|
|
41
|
+
the ticket's own deliveries — comments and labels — name only the
|
|
42
|
+
repository, so `repos` is what claims them for the project. The ticket's
|
|
43
|
+
words live in the repository too: its title, body, comments, reactions, and
|
|
44
|
+
the `needs-human` label. One board is one project's whole board; a
|
|
45
|
+
repository feeds at most one project. The workflow
|
|
46
|
+
is not a GitHub field: it is **core's** lane array on the project,
|
|
47
|
+
`projects.<id>.lanes` — left to right, the order **is** the priority, and the
|
|
48
|
+
names are the board's own `Status` options:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"projects": {
|
|
53
|
+
"website": {
|
|
54
|
+
"tracker-github": { "board": 1, "repos": ["acme/shop"] },
|
|
55
|
+
"lanes": [
|
|
56
|
+
{ "name": "Todo", "queue": true },
|
|
57
|
+
{ "name": "In Progress", "agent": "dev", "worktree": true },
|
|
58
|
+
{ "name": "Review", "agent": "reviewer" }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A lane naming an `agent` is worked; one naming none is worked by humans; a
|
|
66
|
+
Status option named nowhere in the array is silence. `queue: true` marks the
|
|
67
|
+
workflow's **one** queue lane, with the same load rules as Linear's wizard.
|
|
68
|
+
**GitHub says nothing about what a Status option means** — options carry no
|
|
69
|
+
state flag, and a board workflow's rule is not in the API. So the wizard
|
|
70
|
+
asks what every option is, in board order:
|
|
71
|
+
**Agent** (aivi delegates tickets there to an agent), **Human** (aivi ignores
|
|
72
|
+
tickets there; it is only ever used as a queue or a destination), or **None**
|
|
73
|
+
(the option is not part of aivi's workflow: never read, never moved into).
|
|
74
|
+
A ticket's open or closed state is the board's workflows' business; aivi
|
|
75
|
+
never works a closed ticket and ignores everything else about it.
|
|
76
|
+
`aivi projects add` writes the array for you. `board` and `repos` are never
|
|
77
|
+
defaulted.
|
|
78
|
+
|
|
79
|
+
The board's `Status` options are the lanes, and aivi **never creates or
|
|
80
|
+
renames** them (D4): a lane whose option the board does not carry is said, not
|
|
81
|
+
guessed past. The delegate is the board's `Agent` single-select, with the
|
|
82
|
+
option named for the app itself (`aivi-agent`) — apps cannot be assignees on
|
|
83
|
+
GitHub (proven 403), so the board carries the delegation in a field aivi
|
|
84
|
+
created (D5). `aivi projects add` creates that field on a yes and appends it
|
|
85
|
+
to every view's visible fields — the view configuration **replaces**, so each
|
|
86
|
+
view is read first and written back whole; running the wizard twice writes
|
|
87
|
+
nothing the second time. Routing reads the field, not the view; the view is
|
|
88
|
+
so operators can see the delegation.
|
|
89
|
+
|
|
90
|
+
Credentials are never in JSON, and the app is never the tracker's own. The
|
|
91
|
+
forge and the tracker are **one app** (D3): its identity is the bare
|
|
92
|
+
`GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY` in `<home>/.env`, shared with
|
|
93
|
+
`@aivi/forge-github` through `@aivi/github` — installing either asks once. The
|
|
94
|
+
webhook's HMAC secret is `GITHUB_WEBHOOK_SECRET`, beside them — the installer
|
|
95
|
+
asks for it only when the home does not already hold one, and the wait proves
|
|
96
|
+
whichever it has. The app's
|
|
97
|
+
webhook must deliver `issue_comment`, `issues` and `projects_v2_item` to
|
|
98
|
+
`<host.public>/github/webhooks`.
|
|
99
|
+
|
|
100
|
+
`aivi add @aivi/tracker-github` proves the grant before it writes a byte:
|
|
101
|
+
the app's own JWT reads the permissions (Issues and Organization projects,
|
|
102
|
+
both write), the subscribed events, and where the webhook points. Then one
|
|
103
|
+
wait proves the secret against a live delivery: the installer re-asks GitHub
|
|
104
|
+
for the newest `installation` delivery in its last 25 and watches the replay
|
|
105
|
+
land at this aivi's route with a seal that verifies. When the history holds
|
|
106
|
+
none — the app was installed before this webhook pointed here — or GitHub
|
|
107
|
+
refuses the replay, **no wait runs**: a trigger that cannot come is never sat
|
|
108
|
+
out. The person reinstalls the app and retries, or skips webhook
|
|
109
|
+
verification, and the summary says UNVERIFIED. This installer **writes
|
|
110
|
+
nothing at GitHub**. A live aivi is asked as a choice,
|
|
111
|
+
never presumed: no health answer offers "I started the server. Retry" or
|
|
112
|
+
"Continue anyway", and continuing writes the configuration with UNVERIFIED —
|
|
113
|
+
the unproved fact is precisely "GitHub can reach this machine" — in the
|
|
114
|
+
summary. The board itself is probed by `aivi projects add`, where the board
|
|
115
|
+
id is in hand: a test card pays the tracker's every mutation — Status moves
|
|
116
|
+
to the next option in board order the card is not on — and the diary must
|
|
117
|
+
echo each delivery back seal-verified. The eyes reaction is paid and read
|
|
118
|
+
back, not waited for: GitHub posts reaction events only to subscribers, and
|
|
119
|
+
aivi is not one. Then the card is cleared, archived, its reaction and comment
|
|
120
|
+
deleted, and closed not-planned — unless a workflow on the board closed the
|
|
121
|
+
card first, in which case aivi says how GitHub left it. GitHub has no API to
|
|
122
|
+
delete an issue. Restart `aivi serve` to load the tracker.
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aivi/tracker-github",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "The GitHub tracker for aivi: the projectsV2 board the walk reads, the issues a worker works, and the status comment that is the run's conversation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"development": "./src/index.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./config": {
|
|
12
|
+
"development": "./src/config.ts",
|
|
13
|
+
"default": "./dist/config.js"
|
|
14
|
+
},
|
|
15
|
+
"./setup": {
|
|
16
|
+
"development": "./src/setup.ts",
|
|
17
|
+
"default": "./dist/setup.js"
|
|
18
|
+
},
|
|
19
|
+
"./setupProject": {
|
|
20
|
+
"development": "./src/setup-project.ts",
|
|
21
|
+
"default": "./dist/setup-project.js"
|
|
22
|
+
},
|
|
23
|
+
"./cli": {
|
|
24
|
+
"development": "./src/cli.ts",
|
|
25
|
+
"default": "./dist/cli.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"docs"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=26.1.0 <27"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/aivi-hq/aivi.git",
|
|
39
|
+
"directory": "packages/tracker-github"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"prepack": "tsc -b tsconfig.build.json"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@aivi/core": "^0.7.1",
|
|
49
|
+
"@aivi/github": "^0.0.0",
|
|
50
|
+
"@aivi/host": "^0.8.1",
|
|
51
|
+
"@aivi/plugin": "^0.1.0",
|
|
52
|
+
"commander": "15.0.0",
|
|
53
|
+
"zod": "4.1.13"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@aivi/host": "^0.8.1"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@aivi/host": "^0.8.1"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/README.md
DELETED