@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/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
+ }
@@ -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';
@@ -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
+ }
@@ -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
+ }[];