@aivi/tracker-github 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.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": "^1.0.0",
49
+ "@aivi/github": "^1.0.0",
50
+ "@aivi/host": "^1.0.0",
51
+ "@aivi/plugin": "^1.0.0",
52
+ "commander": "15.0.0",
53
+ "zod": "4.1.13"
54
+ },
55
+ "peerDependencies": {
56
+ "@aivi/host": "^1.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@aivi/host": "^1.0.0"
60
+ }
61
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.