@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/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/desk.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GitHub tracker's answer to the ticket desk (the `TicketDesk` contract
|
|
3
|
+
* at `@aivi/plugin/tracker`). The host owns the `aivi_ticket_*` tools and the
|
|
4
|
+
* ground truth of the call — this file answers what each one *is* on GitHub:
|
|
5
|
+
* which ticket a plain comment lands on, which repository a new ticket is
|
|
6
|
+
* born in, what the repository's label catalogue means. GitHub has one app
|
|
7
|
+
* and no sessions to speak through, so every call arrives on the app's own
|
|
8
|
+
* feed: `GITHUB_FEED` is the desk's whole conversation.
|
|
9
|
+
*/
|
|
10
|
+
import type { LoadedConfig } from '@aivi/core';
|
|
11
|
+
import type { Platform, TicketDesk } from '@aivi/plugin/tracker';
|
|
12
|
+
import type { GithubConfig } from './config.ts';
|
|
13
|
+
export declare function createGithubDesk(loaded: LoadedConfig, _config: GithubConfig, platform: Platform): TicketDesk;
|
package/dist/desk.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ToolError } from '@aivi/plugin';
|
|
2
|
+
import { projectGithub } from './projects.js';
|
|
3
|
+
import { GITHUB_FEED } from './tracker.js';
|
|
4
|
+
export function createGithubDesk(loaded, _config, platform) {
|
|
5
|
+
const ticket = (run) => platform.issue(GITHUB_FEED, run.ticketId);
|
|
6
|
+
/** Where a new ticket is born: the project's repositories, the create lane
|
|
7
|
+
* as it takes effect (project's `createLane`, defaults', else the
|
|
8
|
+
* project's first configured lane). An explicit create lane must be a
|
|
9
|
+
* Status option on the board — a wrong name is said, never guessed past;
|
|
10
|
+
* the fallback lane lands where GitHub's own default sends new issues
|
|
11
|
+
* when no board carries its name. */
|
|
12
|
+
const createTarget = async (projectId) => {
|
|
13
|
+
const github = projectGithub(loaded, projectId);
|
|
14
|
+
if (!github?.repos.length)
|
|
15
|
+
throw new ToolError(409, 'this project has no GitHub repositories; aivi_ticket_create has nowhere to create.');
|
|
16
|
+
const explicit = github.createLane;
|
|
17
|
+
const lane = explicit ?? loaded.projects.find(p => p.id === projectId)?.lanes?.[0]?.name;
|
|
18
|
+
if (lane) {
|
|
19
|
+
const states = await platform.laneStates(GITHUB_FEED, github.repos[0]);
|
|
20
|
+
if (states.some(s => s.name === lane))
|
|
21
|
+
return { repo: github.repos[0], lane };
|
|
22
|
+
}
|
|
23
|
+
if (explicit)
|
|
24
|
+
throw new ToolError(409, `createLane "${explicit}" names no Status option on this project's board — fix createLane in config.`);
|
|
25
|
+
return { repo: github.repos[0] };
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
async read(run) {
|
|
29
|
+
const issue = await ticket(run);
|
|
30
|
+
return {
|
|
31
|
+
identifier: issue.identifier,
|
|
32
|
+
title: issue.title,
|
|
33
|
+
description: issue.description,
|
|
34
|
+
lane: issue.state.name,
|
|
35
|
+
labels: issue.labels.map(l => l.name),
|
|
36
|
+
url: issue.url,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
comments(run) {
|
|
40
|
+
return platform.ticketComments(GITHUB_FEED, run.ticketId);
|
|
41
|
+
},
|
|
42
|
+
async labels(run) {
|
|
43
|
+
const issue = await ticket(run);
|
|
44
|
+
return { team: await platform.teamLabels(GITHUB_FEED, issue.teamId), on: issue.labels.map(l => l.name) };
|
|
45
|
+
},
|
|
46
|
+
async edit(run, changes) {
|
|
47
|
+
const issue = await platform.editIssue(GITHUB_FEED, run.ticketId, changes);
|
|
48
|
+
return { identifier: issue.identifier, title: issue.title, url: issue.url };
|
|
49
|
+
},
|
|
50
|
+
comment(run, text) {
|
|
51
|
+
return platform.addComment(GITHUB_FEED, run.ticketId, text);
|
|
52
|
+
},
|
|
53
|
+
async create(run, input) {
|
|
54
|
+
const target = await createTarget(run.projectId);
|
|
55
|
+
const issue = await platform.createIssue(GITHUB_FEED, {
|
|
56
|
+
teamId: target.repo,
|
|
57
|
+
title: input.title,
|
|
58
|
+
description: input.description,
|
|
59
|
+
...(target.lane ? { lane: target.lane } : {}),
|
|
60
|
+
});
|
|
61
|
+
return { identifier: issue.identifier, url: issue.url, lane: issue.state.name };
|
|
62
|
+
},
|
|
63
|
+
async label(run, label, on) {
|
|
64
|
+
await platform.apply(GITHUB_FEED, run.ticketId, { kind: 'label', label, on });
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type { BoardFields, BoardItem, GithubComment, GithubIssue, IssueDependency, IssueRef, ItemSelectValue, } from './client.ts';
|
|
2
|
+
export { dependencyCompleted, GithubClient, githubClient, isGoneError, issueRef, repoIssueId } from './client.ts';
|
|
3
|
+
export type { GithubConfig, GithubProjectDefaults, GithubProjectEntry, ProjectGithub } from './config.ts';
|
|
4
|
+
export { AGENT_FIELD, assistantAgent, githubProjectDefaultsSchema, githubProjectSchema, githubSchema, MODULE_ID, plugin, STATUS_FIELD, } from './config.ts';
|
|
5
|
+
export { createGithubDesk } from './desk.ts';
|
|
6
|
+
export type { RunLink } from './links.ts';
|
|
7
|
+
export { RunLinks } from './links.ts';
|
|
8
|
+
export type { MakeBoard, MakeTracker } from './module.ts';
|
|
9
|
+
export { createGithubModule, describeWorkers, GITHUB, openGithubStore } from './module.ts';
|
|
10
|
+
export type { RoutedProject } from './projects.ts';
|
|
11
|
+
export { githubRepoCollisions, projectForIssue, projectGithub, writeProjectGithub } from './projects.ts';
|
|
12
|
+
export type { RunProgressOptions } from './runprogress.ts';
|
|
13
|
+
export { RunProgress } from './runprogress.ts';
|
|
14
|
+
export type { StatusBody } from './tracker.ts';
|
|
15
|
+
export { GITHUB_FEED, GITHUB_WEBHOOK_PATH, GithubPlatform, issueChanges, parseStatus, registerWebhookRoute, renderStatus, } from './tracker.ts';
|
|
16
|
+
export type { GithubDeliveryHeaders, GithubTrust, IssueCommentPayload, IssuesPayload, ProjectsV2ItemPayload, } from './webhook.ts';
|
|
17
|
+
export { DeliveryLedger, editedFieldName, GITHUB_DELIVERY_HEADER, GITHUB_EVENT_HEADER, isBotAuthor, trustedAuthor, } from './webhook.ts';
|
|
18
|
+
export type { GithubBoard, GithubIssueWords } from './work.ts';
|
|
19
|
+
export { githubBoard, issueDossier } from './work.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { dependencyCompleted, GithubClient, githubClient, isGoneError, issueRef, repoIssueId } from './client.js';
|
|
2
|
+
export { AGENT_FIELD, assistantAgent, githubProjectDefaultsSchema, githubProjectSchema, githubSchema, MODULE_ID, plugin, STATUS_FIELD, } from './config.js';
|
|
3
|
+
export { createGithubDesk } from './desk.js';
|
|
4
|
+
export { RunLinks } from './links.js';
|
|
5
|
+
export { createGithubModule, describeWorkers, GITHUB, openGithubStore } from './module.js';
|
|
6
|
+
export { githubRepoCollisions, projectForIssue, projectGithub, writeProjectGithub } from './projects.js';
|
|
7
|
+
export { RunProgress } from './runprogress.js';
|
|
8
|
+
export { GITHUB_FEED, GITHUB_WEBHOOK_PATH, GithubPlatform, issueChanges, parseStatus, registerWebhookRoute, renderStatus, } from './tracker.js';
|
|
9
|
+
export { DeliveryLedger, editedFieldName, GITHUB_DELIVERY_HEADER, GITHUB_EVENT_HEADER, isBotAuthor, trustedAuthor, } from './webhook.js';
|
|
10
|
+
export { githubBoard, issueDossier } from './work.js';
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GitHub tracker's own durable pair: the **status comment** ↔ the
|
|
3
|
+
* OpenCode session ↔ the ticket. GitHub has no agent session for an app to
|
|
4
|
+
* work in (apps cannot be assignees, proven 403), so the run's conversation
|
|
5
|
+
* is the status comment `startSession` creates (D6) — and the follower keeps
|
|
6
|
+
* that join itself, which is the whole reason a webhook naming a ticket can
|
|
7
|
+
* find its worker's OpenCode session without asking anybody, and the list a
|
|
8
|
+
* boot pass reconciles against GitHub.
|
|
9
|
+
*
|
|
10
|
+
* Its own namespace, its own table (the `Store.migrate` contract every module
|
|
11
|
+
* has: namespaced tables, never host tables). The pair's key is the session
|
|
12
|
+
* id `owner/repo#47:c123` — the ticket with the status comment's id on it.
|
|
13
|
+
*/
|
|
14
|
+
import type { Store } from '@aivi/plugin';
|
|
15
|
+
export interface RunLink {
|
|
16
|
+
agentSession: string;
|
|
17
|
+
ticketId: string;
|
|
18
|
+
opencodeSession?: string;
|
|
19
|
+
/** When the closing landed — the pair is settled and boot asks GitHub
|
|
20
|
+
* about it no more. Absent means the closing is still owed. */
|
|
21
|
+
endedAt?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class RunLinks {
|
|
24
|
+
private readonly store;
|
|
25
|
+
constructor(store: Store);
|
|
26
|
+
/** The status comment exists: record it and its ticket. The OpenCode
|
|
27
|
+
* session joins when the orchestrator's `ready` stage names this ticket's
|
|
28
|
+
* run. */
|
|
29
|
+
bind(agentSession: string, ticketId: string, now?: number): void;
|
|
30
|
+
/** The run's session exists: attach it to the newest unattached pair for
|
|
31
|
+
* the ticket. Absent means the pair was already recorded — a replay of
|
|
32
|
+
* the same session says nothing, never stealing an older pair. */
|
|
33
|
+
attach(ticketId: string, opencodeSession: string): boolean;
|
|
34
|
+
byAgentSession(agentSession: string): RunLink | undefined;
|
|
35
|
+
byOpencodeSession(opencodeSession: string): RunLink | undefined;
|
|
36
|
+
/** The newest pair for a ticket, settled or not: a person's comment on a
|
|
37
|
+
* ticket GitHub names (not a session, as Linear's events do) finds the
|
|
38
|
+
* run it belongs to through here. */
|
|
39
|
+
byTicket(ticketId: string): RunLink | undefined;
|
|
40
|
+
/** Every pair whose closing has not landed: the boot pass reads the run
|
|
41
|
+
* state for each and catches GitHub up wherever the run has ended. A
|
|
42
|
+
* settled pair is absent — its closing spoke GitHub's own state once, and
|
|
43
|
+
* asking again forever was Linear's bug (live, 2026-10-06). Follow-up
|
|
44
|
+
* routing never comes through here: it asks `byTicket` directly, settled
|
|
45
|
+
* or not. The lane move is not this list's business: it is the
|
|
46
|
+
* orchestrator's own debt in its own ledger. */
|
|
47
|
+
owed(): RunLink[];
|
|
48
|
+
/** The closing landed: settle the pair. The row stays — it is still the
|
|
49
|
+
* route a person's later message finds its run through. */
|
|
50
|
+
closingLanded(agentSession: string, now?: number): void;
|
|
51
|
+
/** Retire a pair for good. Its use today is the gone-closing: GitHub
|
|
52
|
+
* answers the status comment with `404` — the ticket was deleted — so
|
|
53
|
+
* there is nothing left to pay and no reason for the next boot to ask
|
|
54
|
+
* again. */
|
|
55
|
+
drop(agentSession: string): void;
|
|
56
|
+
}
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const map = (r) => ({
|
|
2
|
+
agentSession: String(r.agent_session),
|
|
3
|
+
ticketId: String(r.ticket_id),
|
|
4
|
+
...(r.opencode_session === null ? {} : { opencodeSession: String(r.opencode_session) }),
|
|
5
|
+
...(r.ended_at === null ? {} : { endedAt: Number(r.ended_at) }),
|
|
6
|
+
});
|
|
7
|
+
const migrations = [
|
|
8
|
+
`CREATE TABLE tracker_github_run_links(
|
|
9
|
+
agent_session TEXT PRIMARY KEY, ticket_id TEXT NOT NULL, opencode_session TEXT UNIQUE,
|
|
10
|
+
created_at INTEGER NOT NULL);
|
|
11
|
+
CREATE INDEX tracker_github_run_links_ticket ON tracker_github_run_links(ticket_id);`,
|
|
12
|
+
// The boot pass reads only the pairs whose closing has not landed, so the
|
|
13
|
+
// index is **partial**: it holds the owed rows and nothing else, and the
|
|
14
|
+
// settled history never weighs on a boot.
|
|
15
|
+
`ALTER TABLE tracker_github_run_links ADD COLUMN ended_at INTEGER;
|
|
16
|
+
CREATE INDEX tracker_github_run_links_owed ON tracker_github_run_links(created_at) WHERE ended_at IS NULL;`,
|
|
17
|
+
];
|
|
18
|
+
export class RunLinks {
|
|
19
|
+
store;
|
|
20
|
+
constructor(store) {
|
|
21
|
+
this.store = store;
|
|
22
|
+
store.migrate('tracker-github', migrations);
|
|
23
|
+
}
|
|
24
|
+
/** The status comment exists: record it and its ticket. The OpenCode
|
|
25
|
+
* session joins when the orchestrator's `ready` stage names this ticket's
|
|
26
|
+
* run. */
|
|
27
|
+
bind(agentSession, ticketId, now = Date.now()) {
|
|
28
|
+
this.store.db
|
|
29
|
+
.prepare(`INSERT INTO tracker_github_run_links(agent_session, ticket_id, created_at) VALUES(?,?,?)
|
|
30
|
+
ON CONFLICT(agent_session) DO UPDATE SET ticket_id=excluded.ticket_id`)
|
|
31
|
+
.run(agentSession, ticketId, now);
|
|
32
|
+
}
|
|
33
|
+
/** The run's session exists: attach it to the newest unattached pair for
|
|
34
|
+
* the ticket. Absent means the pair was already recorded — a replay of
|
|
35
|
+
* the same session says nothing, never stealing an older pair. */
|
|
36
|
+
attach(ticketId, opencodeSession) {
|
|
37
|
+
const changed = this.store.db
|
|
38
|
+
.prepare(`UPDATE tracker_github_run_links SET opencode_session=?
|
|
39
|
+
WHERE agent_session = (
|
|
40
|
+
SELECT agent_session FROM tracker_github_run_links
|
|
41
|
+
WHERE ticket_id=? AND opencode_session IS NULL ORDER BY created_at DESC LIMIT 1)
|
|
42
|
+
AND opencode_session IS NULL
|
|
43
|
+
AND NOT EXISTS (SELECT 1 FROM tracker_github_run_links WHERE opencode_session=?)`)
|
|
44
|
+
.run(opencodeSession, ticketId, opencodeSession);
|
|
45
|
+
return changed.changes > 0;
|
|
46
|
+
}
|
|
47
|
+
byAgentSession(agentSession) {
|
|
48
|
+
const row = this.store.db
|
|
49
|
+
.prepare('SELECT * FROM tracker_github_run_links WHERE agent_session=?')
|
|
50
|
+
.get(agentSession);
|
|
51
|
+
return row && map(row);
|
|
52
|
+
}
|
|
53
|
+
byOpencodeSession(opencodeSession) {
|
|
54
|
+
const row = this.store.db
|
|
55
|
+
.prepare('SELECT * FROM tracker_github_run_links WHERE opencode_session=?')
|
|
56
|
+
.get(opencodeSession);
|
|
57
|
+
return row && map(row);
|
|
58
|
+
}
|
|
59
|
+
/** The newest pair for a ticket, settled or not: a person's comment on a
|
|
60
|
+
* ticket GitHub names (not a session, as Linear's events do) finds the
|
|
61
|
+
* run it belongs to through here. */
|
|
62
|
+
byTicket(ticketId) {
|
|
63
|
+
const row = this.store.db
|
|
64
|
+
.prepare('SELECT * FROM tracker_github_run_links WHERE ticket_id=? ORDER BY created_at DESC LIMIT 1')
|
|
65
|
+
.get(ticketId);
|
|
66
|
+
return row && map(row);
|
|
67
|
+
}
|
|
68
|
+
/** Every pair whose closing has not landed: the boot pass reads the run
|
|
69
|
+
* state for each and catches GitHub up wherever the run has ended. A
|
|
70
|
+
* settled pair is absent — its closing spoke GitHub's own state once, and
|
|
71
|
+
* asking again forever was Linear's bug (live, 2026-10-06). Follow-up
|
|
72
|
+
* routing never comes through here: it asks `byTicket` directly, settled
|
|
73
|
+
* or not. The lane move is not this list's business: it is the
|
|
74
|
+
* orchestrator's own debt in its own ledger. */
|
|
75
|
+
owed() {
|
|
76
|
+
return this.store.db
|
|
77
|
+
.prepare('SELECT * FROM tracker_github_run_links WHERE opencode_session IS NOT NULL AND ended_at IS NULL ORDER BY created_at')
|
|
78
|
+
.all().map(map);
|
|
79
|
+
}
|
|
80
|
+
/** The closing landed: settle the pair. The row stays — it is still the
|
|
81
|
+
* route a person's later message finds its run through. */
|
|
82
|
+
closingLanded(agentSession, now = Date.now()) {
|
|
83
|
+
this.store.db
|
|
84
|
+
.prepare('UPDATE tracker_github_run_links SET ended_at=? WHERE agent_session=?')
|
|
85
|
+
.run(now, agentSession);
|
|
86
|
+
}
|
|
87
|
+
/** Retire a pair for good. Its use today is the gone-closing: GitHub
|
|
88
|
+
* answers the status comment with `404` — the ticket was deleted — so
|
|
89
|
+
* there is nothing left to pay and no reason for the next boot to ask
|
|
90
|
+
* again. */
|
|
91
|
+
drop(agentSession) {
|
|
92
|
+
this.store.db.prepare('DELETE FROM tracker_github_run_links WHERE agent_session=?').run(agentSession);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ConversationStore } from '@aivi/host';
|
|
2
|
+
import type { AiviModule, AiviServices, Store } from '@aivi/plugin';
|
|
3
|
+
import type { ChannelPlatform, Turn } from '@aivi/plugin/channel';
|
|
4
|
+
import type { GithubConfig } from './config.ts';
|
|
5
|
+
import { GithubPlatform } from './tracker.ts';
|
|
6
|
+
import type { GithubBoard } from './work.ts';
|
|
7
|
+
/**
|
|
8
|
+
* The platform: a ticket is an assistant conversation, a status comment is a
|
|
9
|
+
* run conversation — `github:owner/repo#47` and `github:owner/repo#47:c123`.
|
|
10
|
+
* Assistant turns have effects beyond their reply, so a restart mid-turn
|
|
11
|
+
* blocks instead of discarding.
|
|
12
|
+
*/
|
|
13
|
+
export declare const GITHUB: ChannelPlatform;
|
|
14
|
+
export declare function openGithubStore(store: Store): ConversationStore;
|
|
15
|
+
/** Where the module's tracker comes from: GitHub's adapter by default, a
|
|
16
|
+
* fake tracker in a test of the decisions below. The concrete class is the
|
|
17
|
+
* point — the closing speaks the GitHub-only words (`markClosing`,
|
|
18
|
+
* `freezeStatus`) the neutral contract does not carry. */
|
|
19
|
+
export type MakeTracker = (services: AiviServices) => Promise<GithubPlatform>;
|
|
20
|
+
/** The board, pull-shaped, for the orchestrator's walk; a test hands its own
|
|
21
|
+
* lanes the way it hands its own tracker. */
|
|
22
|
+
export type MakeBoard = (services: AiviServices, tracker: GithubPlatform) => GithubBoard;
|
|
23
|
+
export declare function createGithubModule(config: GithubConfig, makeTracker?: MakeTracker, makeBoard?: MakeBoard): AiviModule;
|
|
24
|
+
/** The branch a ticket works on (D8): GitHub names none, so the tracker
|
|
25
|
+
* generates `aivi/<number>-<slug>` — the words a worktree lane checks out,
|
|
26
|
+
* human-readable and stable across retries. */
|
|
27
|
+
export declare function branchFor(ticketId: string, title: string): string;
|
|
28
|
+
/** For `aivi github status`: what each assistant conversation is doing.
|
|
29
|
+
* Workers live in the orchestrator's ledger now; their lines join this view
|
|
30
|
+
* when the status command is formalized with the extraction. */
|
|
31
|
+
export declare function describeWorkers(store: ConversationStore): {
|
|
32
|
+
conversation: string;
|
|
33
|
+
agent: string | null;
|
|
34
|
+
issue: string | null;
|
|
35
|
+
turn: Turn;
|
|
36
|
+
}[];
|