@atmin.ai/review 0.1.0-alpha.2
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/DEPENDENCIES.md +15 -0
- package/LICENSE +55 -0
- package/NOTICE +6 -0
- package/README.md +144 -0
- package/dist/assessment.d.ts +26 -0
- package/dist/assessment.js +88 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +100 -0
- package/dist/contracts.d.ts +625 -0
- package/dist/contracts.js +86 -0
- package/dist/cost-report.d.ts +40 -0
- package/dist/cost-report.js +42 -0
- package/dist/github/api.d.ts +66 -0
- package/dist/github/api.js +190 -0
- package/dist/github/checks.d.ts +11 -0
- package/dist/github/checks.js +42 -0
- package/dist/github/cli.d.ts +2 -0
- package/dist/github/cli.js +94 -0
- package/dist/github/config.d.ts +16 -0
- package/dist/github/config.js +41 -0
- package/dist/github/runner.d.ts +7 -0
- package/dist/github/runner.js +67 -0
- package/dist/github/store.d.ts +40 -0
- package/dist/github/store.js +134 -0
- package/dist/github/task.d.ts +1 -0
- package/dist/github/task.js +29 -0
- package/dist/github/webhook.d.ts +6 -0
- package/dist/github/webhook.js +119 -0
- package/dist/github/worker.d.ts +19 -0
- package/dist/github/worker.js +209 -0
- package/dist/investigation.d.ts +198 -0
- package/dist/investigation.js +276 -0
- package/dist/openai-model.d.ts +2 -0
- package/dist/openai-model.js +41 -0
- package/dist/openrouter-model.d.ts +2 -0
- package/dist/openrouter-model.js +90 -0
- package/dist/provider-error.d.ts +10 -0
- package/dist/provider-error.js +21 -0
- package/dist/render.d.ts +4 -0
- package/dist/render.js +56 -0
- package/dist/run.d.ts +6 -0
- package/dist/run.js +39 -0
- package/dist/snapshot.d.ts +46 -0
- package/dist/snapshot.js +246 -0
- package/package.json +47 -0
- package/profiles/baseline-deepseek.json +10 -0
- package/profiles/smoke-openai.json +10 -0
- package/profiles/smoke-openrouter-free.json +10 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
export type JobState = 'queued' | 'running' | 'publishing' | 'completed' | 'failed' | 'cancelled' | 'skipped' | 'uncertain';
|
|
3
|
+
export interface Job {
|
|
4
|
+
id: string;
|
|
5
|
+
pr: number;
|
|
6
|
+
state: JobState;
|
|
7
|
+
created: number;
|
|
8
|
+
started: number | null;
|
|
9
|
+
artifact: string | null;
|
|
10
|
+
report: string | null;
|
|
11
|
+
error: string | null;
|
|
12
|
+
createStarted: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class Store {
|
|
15
|
+
readonly db: DatabaseSync;
|
|
16
|
+
constructor(directory: string);
|
|
17
|
+
close(): void;
|
|
18
|
+
transaction<T>(fn: () => T): T;
|
|
19
|
+
enabled(): boolean;
|
|
20
|
+
enable(value: boolean): void;
|
|
21
|
+
seen(delivery: string): boolean;
|
|
22
|
+
enqueue(delivery: string, pr: number): string | null;
|
|
23
|
+
acquire(owner: string, now?: number): boolean;
|
|
24
|
+
renew(owner: string): boolean;
|
|
25
|
+
release(owner: string): void;
|
|
26
|
+
owns(owner: string): boolean;
|
|
27
|
+
current(job: Job, owner: string): boolean;
|
|
28
|
+
next(owner: string): Job | null;
|
|
29
|
+
reserve(job: Job, owner: string, limit: number): boolean;
|
|
30
|
+
get(id: string): Job;
|
|
31
|
+
update(id: string, values: Partial<Pick<Job, 'state' | 'artifact' | 'report' | 'error' | 'createStarted'>>): void;
|
|
32
|
+
comment(pr: number): number | null;
|
|
33
|
+
track(pr: number, baseRef: string, active: boolean): void;
|
|
34
|
+
branchPulls(baseRef: string): number[];
|
|
35
|
+
setComment(pr: number, id: number): void;
|
|
36
|
+
uncertainCreate(pr: number): boolean;
|
|
37
|
+
creationConfirmed(pr: number, comment: number): void;
|
|
38
|
+
retryPublication(pr: number): boolean;
|
|
39
|
+
status(): unknown;
|
|
40
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { chmodSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
// One service owns the worker lease. Local CLI controls share the WAL database.
|
|
6
|
+
export class Store {
|
|
7
|
+
db;
|
|
8
|
+
constructor(directory) {
|
|
9
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
10
|
+
chmodSync(directory, 0o700);
|
|
11
|
+
const path = join(directory, 'pilot.sqlite');
|
|
12
|
+
this.db = new DatabaseSync(path);
|
|
13
|
+
chmodSync(path, 0o600);
|
|
14
|
+
this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;
|
|
15
|
+
CREATE TABLE IF NOT EXISTS control (id INTEGER PRIMARY KEY CHECK(id=1), enabled INTEGER NOT NULL DEFAULT 0, owner TEXT, lease INTEGER NOT NULL DEFAULT 0);
|
|
16
|
+
INSERT OR IGNORE INTO control(id) VALUES(1);
|
|
17
|
+
CREATE TABLE IF NOT EXISTS deliveries (id TEXT PRIMARY KEY, received INTEGER NOT NULL);
|
|
18
|
+
CREATE TABLE IF NOT EXISTS jobs (id TEXT PRIMARY KEY, pr INTEGER NOT NULL, state TEXT NOT NULL, created INTEGER NOT NULL, started INTEGER, artifact TEXT, report TEXT, error TEXT, createStarted INTEGER NOT NULL DEFAULT 0);
|
|
19
|
+
CREATE TABLE IF NOT EXISTS pulls (pr INTEGER PRIMARY KEY, desired TEXT NOT NULL, comment INTEGER, baseRef TEXT, active INTEGER NOT NULL DEFAULT 1);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS jobs_state ON jobs(state, created);`);
|
|
21
|
+
}
|
|
22
|
+
close() { this.db.close(); }
|
|
23
|
+
transaction(fn) {
|
|
24
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
25
|
+
try {
|
|
26
|
+
const value = fn();
|
|
27
|
+
this.db.exec('COMMIT');
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
this.db.exec('ROLLBACK');
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
enabled() { return Boolean(this.db.prepare('SELECT enabled FROM control').get().enabled); }
|
|
36
|
+
enable(value) {
|
|
37
|
+
this.transaction(() => {
|
|
38
|
+
this.db.prepare('UPDATE control SET enabled=?').run(Number(value));
|
|
39
|
+
if (!value)
|
|
40
|
+
this.db.prepare("UPDATE jobs SET state='cancelled', error='operator-paused' WHERE state IN ('queued','running','publishing')").run();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
seen(delivery) { return Boolean(this.db.prepare('SELECT 1 FROM deliveries WHERE id=?').get(delivery)); }
|
|
44
|
+
enqueue(delivery, pr) {
|
|
45
|
+
return this.transaction(() => {
|
|
46
|
+
if (this.seen(delivery))
|
|
47
|
+
return null;
|
|
48
|
+
this.db.prepare('INSERT INTO deliveries VALUES(?,?)').run(delivery, Date.now());
|
|
49
|
+
if (!this.enabled())
|
|
50
|
+
return null;
|
|
51
|
+
const id = randomUUID();
|
|
52
|
+
this.db.prepare("UPDATE jobs SET state='cancelled', error='superseded' WHERE pr=? AND state IN ('queued','running','publishing')").run(pr);
|
|
53
|
+
this.db.prepare("INSERT INTO jobs(id,pr,state,created) VALUES(?,?,'queued',?)").run(id, pr, Date.now());
|
|
54
|
+
this.db.prepare('INSERT INTO pulls(pr,desired) VALUES(?,?) ON CONFLICT(pr) DO UPDATE SET desired=excluded.desired').run(pr, id);
|
|
55
|
+
return id;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
acquire(owner, now = Date.now()) {
|
|
59
|
+
return this.transaction(() => {
|
|
60
|
+
const control = this.db.prepare('SELECT owner,lease FROM control').get();
|
|
61
|
+
if (control.owner !== owner && Number(control.lease) > now)
|
|
62
|
+
return false;
|
|
63
|
+
// Never restart interrupted inference automatically: its reservation may have been consumed.
|
|
64
|
+
if (control.owner !== owner)
|
|
65
|
+
this.db.prepare("UPDATE jobs SET state=CASE WHEN report IS NULL THEN 'failed' ELSE 'publishing' END, error='worker-interrupted; explicit rerun required' WHERE state='running'").run();
|
|
66
|
+
this.db.prepare('UPDATE control SET owner=?,lease=?').run(owner, now + 30_000);
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
renew(owner) {
|
|
71
|
+
return this.db.prepare('UPDATE control SET lease=? WHERE owner=? AND lease>?').run(Date.now() + 30_000, owner, Date.now()).changes === 1;
|
|
72
|
+
}
|
|
73
|
+
release(owner) { this.db.prepare('UPDATE control SET owner=NULL,lease=0 WHERE owner=?').run(owner); }
|
|
74
|
+
owns(owner) { return Boolean(this.db.prepare('SELECT 1 FROM control WHERE owner=? AND lease>?').get(owner, Date.now())); }
|
|
75
|
+
current(job, owner) {
|
|
76
|
+
return this.enabled() && this.owns(owner) && Boolean(this.db.prepare("SELECT 1 FROM pulls JOIN jobs ON pulls.desired=jobs.id WHERE jobs.id=? AND jobs.state IN ('running','publishing')").get(job.id));
|
|
77
|
+
}
|
|
78
|
+
next(owner) {
|
|
79
|
+
return this.transaction(() => {
|
|
80
|
+
if (!this.enabled() || !this.owns(owner))
|
|
81
|
+
return null;
|
|
82
|
+
const resume = this.db.prepare("SELECT * FROM jobs WHERE state='publishing' ORDER BY created LIMIT 1").get();
|
|
83
|
+
if (resume)
|
|
84
|
+
return resume;
|
|
85
|
+
const job = this.db.prepare("SELECT * FROM jobs WHERE state='queued' ORDER BY created LIMIT 1").get();
|
|
86
|
+
if (!job)
|
|
87
|
+
return null;
|
|
88
|
+
this.db.prepare("UPDATE jobs SET state='running' WHERE id=?").run(job.id);
|
|
89
|
+
return this.get(job.id);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
reserve(job, owner, limit) {
|
|
93
|
+
return this.transaction(() => {
|
|
94
|
+
if (!this.current(job, owner))
|
|
95
|
+
return false;
|
|
96
|
+
const count = Number(this.db.prepare('SELECT count(*) AS n FROM jobs WHERE started>?').get(Date.now() - 86_400_000).n);
|
|
97
|
+
if (count >= limit)
|
|
98
|
+
return false;
|
|
99
|
+
this.db.prepare('UPDATE jobs SET started=? WHERE id=?').run(Date.now(), job.id);
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
get(id) { return this.db.prepare('SELECT * FROM jobs WHERE id=?').get(id); }
|
|
104
|
+
update(id, values) {
|
|
105
|
+
const entries = Object.entries(values);
|
|
106
|
+
if (!entries.length)
|
|
107
|
+
return;
|
|
108
|
+
const allowed = ['state', 'artifact', 'report', 'error', 'createStarted'];
|
|
109
|
+
if (entries.some(([key]) => !allowed.includes(key)))
|
|
110
|
+
throw new Error('Invalid job field');
|
|
111
|
+
this.db.prepare(`UPDATE jobs SET ${entries.map(([key]) => `${key}=?`).join(',')} WHERE id=?`).run(...entries.map(([, v]) => v), id);
|
|
112
|
+
}
|
|
113
|
+
comment(pr) { return this.db.prepare('SELECT comment FROM pulls WHERE pr=?').get(pr)?.comment ?? null; }
|
|
114
|
+
track(pr, baseRef, active) { this.db.prepare('UPDATE pulls SET baseRef=?,active=? WHERE pr=?').run(baseRef, Number(active), pr); }
|
|
115
|
+
branchPulls(baseRef) { return this.db.prepare('SELECT pr FROM pulls WHERE baseRef=? AND active=1').all(baseRef).map(row => Number(row.pr)); }
|
|
116
|
+
setComment(pr, id) { this.db.prepare('UPDATE pulls SET comment=? WHERE pr=?').run(id, pr); }
|
|
117
|
+
uncertainCreate(pr) { return Boolean(this.db.prepare('SELECT 1 FROM jobs WHERE pr=? AND createStarted=1').get(pr)); }
|
|
118
|
+
creationConfirmed(pr, comment) {
|
|
119
|
+
this.transaction(() => {
|
|
120
|
+
this.setComment(pr, comment);
|
|
121
|
+
this.db.prepare('UPDATE jobs SET createStarted=0 WHERE pr=?').run(pr);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
retryPublication(pr) {
|
|
125
|
+
const job = this.db.prepare("SELECT jobs.* FROM jobs JOIN pulls ON jobs.id=pulls.desired WHERE jobs.pr=? AND jobs.report IS NOT NULL AND jobs.state IN ('uncertain','failed','completed')").get(pr);
|
|
126
|
+
if (!job)
|
|
127
|
+
return false;
|
|
128
|
+
this.update(job.id, { state: 'publishing', error: null });
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
status() {
|
|
132
|
+
return { enabled: this.enabled(), jobs: this.db.prepare('SELECT id,pr,state,created,started,error,artifact FROM jobs ORDER BY created DESC LIMIT 30').all() };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Controlled child entry point: source credentials and inference credentials never coexist.
|
|
2
|
+
import { prepare, loadReview, withGitDeadline } from '../snapshot.js';
|
|
3
|
+
import { runReview, readProfile } from '../run.js';
|
|
4
|
+
const [phase, input, output] = process.argv.slice(2);
|
|
5
|
+
const abort = new AbortController();
|
|
6
|
+
const parent = process.ppid;
|
|
7
|
+
const parentMonitor = setInterval(() => { if (process.ppid !== parent)
|
|
8
|
+
abort.abort(); }, 1000);
|
|
9
|
+
process.once('SIGTERM', () => abort.abort());
|
|
10
|
+
process.once('SIGINT', () => abort.abort());
|
|
11
|
+
try {
|
|
12
|
+
if (!input || !output)
|
|
13
|
+
throw new Error('Invalid child arguments');
|
|
14
|
+
if (phase === 'capture')
|
|
15
|
+
withGitDeadline(120_000, () => prepare(input, output));
|
|
16
|
+
else if (phase === 'investigate') {
|
|
17
|
+
await runReview(input, readProfile(output), undefined, abort.signal);
|
|
18
|
+
withGitDeadline(30_000, () => loadReview(input));
|
|
19
|
+
}
|
|
20
|
+
else
|
|
21
|
+
throw new Error('Invalid child phase');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// The supervisor records phase failure; model/provider bodies and source stay in private artifacts.
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
clearInterval(parentMonitor);
|
|
29
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Server } from 'node:http';
|
|
2
|
+
import type { PilotConfig } from './config.js';
|
|
3
|
+
import type { GitHub } from './api.js';
|
|
4
|
+
import type { Store } from './store.js';
|
|
5
|
+
export declare function validSignature(body: Buffer, header: string | undefined, secret: string): boolean;
|
|
6
|
+
export declare function webhook(config: PilotConfig, secret: string, store: Store, github: GitHub): Server;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
export function validSignature(body, header, secret) {
|
|
4
|
+
if (!header || !/^sha256=[a-f0-9]{64}$/.test(header))
|
|
5
|
+
return false;
|
|
6
|
+
return timingSafeEqual(Buffer.from(header.slice(7), 'hex'), createHmac('sha256', secret).update(body).digest());
|
|
7
|
+
}
|
|
8
|
+
export function webhook(config, secret, store, github) {
|
|
9
|
+
return createServer({ requestTimeout: 10_000, headersTimeout: 10_000 }, async (request, response) => {
|
|
10
|
+
const reply = (code, text) => { response.writeHead(code, { 'Content-Type': 'text/plain', 'Cache-Control': 'no-store' }); response.end(`${text}\n`); };
|
|
11
|
+
if (request.method === 'GET' && request.url === '/healthz') {
|
|
12
|
+
reply(200, 'ok');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (request.method !== 'POST' || request.url !== '/webhooks/github') {
|
|
16
|
+
reply(404, 'not found');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let size = 0;
|
|
22
|
+
for await (const chunk of request) {
|
|
23
|
+
size += chunk.length;
|
|
24
|
+
if (size > 1_048_576) {
|
|
25
|
+
reply(413, 'payload too large');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
chunks.push(chunk);
|
|
29
|
+
}
|
|
30
|
+
const body = Buffer.concat(chunks);
|
|
31
|
+
const signature = request.headers['x-hub-signature-256'];
|
|
32
|
+
if (typeof signature !== 'string' || !validSignature(body, signature, secret)) {
|
|
33
|
+
reply(401, 'invalid signature');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const delivery = request.headers['x-github-delivery'];
|
|
37
|
+
if (typeof delivery !== 'string' || !/^[a-zA-Z0-9-]{1,100}$/.test(delivery)) {
|
|
38
|
+
reply(400, 'invalid delivery');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const event = request.headers['x-github-event'];
|
|
42
|
+
let payload;
|
|
43
|
+
try {
|
|
44
|
+
payload = JSON.parse(body.toString('utf8'));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
reply(400, 'invalid JSON');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!payload || typeof payload !== 'object') {
|
|
51
|
+
reply(400, 'invalid payload');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (event === 'ping') {
|
|
55
|
+
reply(200, 'pong');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (payload.installation?.id !== config.installationId) {
|
|
59
|
+
reply(202, 'ignored installation');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if ((event === 'installation' && ['deleted', 'suspend'].includes(payload.action)) || (event === 'installation_repositories' && payload.repositories_removed?.some((repo) => repo.id === config.repositoryId))) {
|
|
63
|
+
store.enable(false);
|
|
64
|
+
reply(202, 'paused');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (payload.repository?.id !== config.repositoryId || payload.repository?.full_name !== config.repository) {
|
|
68
|
+
reply(202, 'ignored repository');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (store.seen(delivery)) {
|
|
72
|
+
reply(200, 'duplicate');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (event === 'push' && typeof payload.ref === 'string' && payload.ref.startsWith('refs/heads/')) {
|
|
76
|
+
for (const number of store.branchPulls(payload.ref.slice(11)))
|
|
77
|
+
store.enqueue(`${delivery}-${number}`, number);
|
|
78
|
+
reply(202, 'tracked target branches reconciled');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let pr;
|
|
82
|
+
if (event === 'pull_request' && ['opened', 'reopened', 'synchronize', 'ready_for_review', 'converted_to_draft', 'closed'].includes(payload.action))
|
|
83
|
+
pr = payload.number;
|
|
84
|
+
if (event === 'pull_request' && payload.action === 'edited' && payload.changes?.base)
|
|
85
|
+
pr = payload.number;
|
|
86
|
+
if (event === 'issue_comment' && payload.action === 'created' && payload.issue?.pull_request && payload.comment?.body?.trim() === '/atmin review') {
|
|
87
|
+
if (payload.comment.user?.type !== 'User' || payload.sender?.id !== payload.comment.user?.id) {
|
|
88
|
+
reply(202, 'ignored actor');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// GitHub expects a prompt acknowledgement. On timeout, accept nothing and allow redelivery.
|
|
92
|
+
let timer;
|
|
93
|
+
let allowed;
|
|
94
|
+
try {
|
|
95
|
+
allowed = await Promise.race([github.canReview(payload.comment.user.login), new Promise((_, reject) => {
|
|
96
|
+
timer = setTimeout(() => reject(new Error('Permission lookup deadline')), 8000);
|
|
97
|
+
})]);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
if (!allowed) {
|
|
103
|
+
reply(202, 'insufficient permission');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
pr = payload.issue.number;
|
|
107
|
+
}
|
|
108
|
+
if (!Number.isSafeInteger(pr) || pr < 1) {
|
|
109
|
+
reply(202, 'ignored event');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const id = store.enqueue(delivery, pr);
|
|
113
|
+
reply(202, id ? 'queued' : 'paused or duplicate');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
reply(503, 'delivery not accepted; redeliver after recovery');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PilotConfig } from './config.js';
|
|
2
|
+
import type { GitHub } from './api.js';
|
|
3
|
+
import type { Runner } from './runner.js';
|
|
4
|
+
import { Store } from './store.js';
|
|
5
|
+
export declare const markerFor: (repo: number, pr: number) => string;
|
|
6
|
+
export declare class Worker {
|
|
7
|
+
private config;
|
|
8
|
+
private store;
|
|
9
|
+
private github;
|
|
10
|
+
private run;
|
|
11
|
+
readonly owner: string;
|
|
12
|
+
private checks;
|
|
13
|
+
private abort;
|
|
14
|
+
constructor(config: PilotConfig, store: Store, github: GitHub, run: Runner, owner: string);
|
|
15
|
+
stop(): void;
|
|
16
|
+
tick(): Promise<boolean>;
|
|
17
|
+
private work;
|
|
18
|
+
private cancel;
|
|
19
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parsePacket, parseResult } from '../contracts.js';
|
|
4
|
+
import { assess } from '../assessment.js';
|
|
5
|
+
import { compareCurrent } from '../snapshot.js';
|
|
6
|
+
import { renderMarkdown } from '../render.js';
|
|
7
|
+
import { Checks, assessmentCheck } from './checks.js';
|
|
8
|
+
export const markerFor = (repo, pr) => `<!-- atmin-review:${repo}:${pr} -->`;
|
|
9
|
+
const same = (a, b) => a.headSha === b.headSha && a.baseSha === b.baseSha && a.baseRef === b.baseRef && a.state === b.state && a.draft === b.draft;
|
|
10
|
+
export class Worker {
|
|
11
|
+
config;
|
|
12
|
+
store;
|
|
13
|
+
github;
|
|
14
|
+
run;
|
|
15
|
+
owner;
|
|
16
|
+
checks;
|
|
17
|
+
abort;
|
|
18
|
+
constructor(config, store, github, run, owner) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.store = store;
|
|
21
|
+
this.github = github;
|
|
22
|
+
this.run = run;
|
|
23
|
+
this.owner = owner;
|
|
24
|
+
this.checks = new Checks(store, github);
|
|
25
|
+
}
|
|
26
|
+
stop() { this.abort?.abort(); }
|
|
27
|
+
async tick() {
|
|
28
|
+
const job = this.store.next(this.owner);
|
|
29
|
+
if (!job)
|
|
30
|
+
return false;
|
|
31
|
+
this.abort = new AbortController();
|
|
32
|
+
const monitor = setInterval(() => { if (!this.store.current(job, this.owner))
|
|
33
|
+
this.abort?.abort(); }, 250);
|
|
34
|
+
try {
|
|
35
|
+
await this.work(job, this.abort.signal);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
if (this.store.current(job, this.owner))
|
|
39
|
+
this.store.update(job.id, { state: 'failed', error: 'service-or-github-failure; inspect private artifacts, rerun explicitly' });
|
|
40
|
+
if (this.store.owns(this.owner))
|
|
41
|
+
await this.checks.stop(job, 'failure', 'Review failed').catch(() => { });
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
clearInterval(monitor);
|
|
45
|
+
this.abort = undefined;
|
|
46
|
+
if (this.store.owns(this.owner) && this.store.get(job.id).state === 'cancelled') {
|
|
47
|
+
await this.checks.stop(job, 'cancelled', 'Review superseded or paused').catch(() => { });
|
|
48
|
+
if (!this.store.enabled()) {
|
|
49
|
+
const marker = markerFor(this.config.repositoryId, job.pr);
|
|
50
|
+
const existing = await this.github.summary(job.pr, marker);
|
|
51
|
+
if (existing && this.store.owns(this.owner) && !this.store.enabled()) {
|
|
52
|
+
await this.github.update(existing.id, `${marker}\n# atmin review — review paused\n\nThe operator paused this review. No active review or completed result is claimed for this request. Previous findings are historical. After re-enabling the service, a maintainer can request a new run with \`/atmin review\`.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (this.store.owns(this.owner) && this.store.get(job.id).state === 'uncertain')
|
|
57
|
+
await this.checks.stop(job, 'failure', 'Review publication uncertain').catch(() => { });
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
async work(job, signal) {
|
|
62
|
+
const initial = await this.github.pull(job.pr);
|
|
63
|
+
if (!this.store.current(job, this.owner) || signal.aborted)
|
|
64
|
+
return;
|
|
65
|
+
this.store.track(job.pr, initial.baseRef, initial.state === 'open' && !initial.draft);
|
|
66
|
+
const marker = markerFor(this.config.repositoryId, job.pr);
|
|
67
|
+
if (initial.state !== 'open' || initial.draft) {
|
|
68
|
+
const existing = await this.github.summary(job.pr, marker);
|
|
69
|
+
if (existing && this.store.current(job, this.owner))
|
|
70
|
+
await this.github.update(existing.id, `${marker}\n# atmin review — inactive\n\nPR is ${initial.draft ? 'a draft' : 'closed'}. Previous findings are historical. No merge approval.`);
|
|
71
|
+
if (this.store.current(job, this.owner))
|
|
72
|
+
this.store.update(job.id, { state: 'skipped', error: initial.draft ? 'draft' : 'closed' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (job.state !== 'publishing') {
|
|
76
|
+
// Retire an earlier verdict as soon as replacement work starts. Create only the final summary.
|
|
77
|
+
const existing = await this.github.summary(job.pr, marker);
|
|
78
|
+
if (existing && this.store.current(job, this.owner)) {
|
|
79
|
+
this.store.creationConfirmed(job.pr, existing.id);
|
|
80
|
+
await this.github.update(existing.id, `${marker}\n# atmin review — review pending\n\nA review was requested for head \`${initial.headSha}\` against target \`${initial.baseSha}\`. No completed report is available for this request. Previous findings are historical. This review is advisory.`);
|
|
81
|
+
}
|
|
82
|
+
if (!this.store.current(job, this.owner) || signal.aborted)
|
|
83
|
+
return;
|
|
84
|
+
let report;
|
|
85
|
+
let check = { status: 'completed', conclusion: 'failure', output: { title: 'Review incomplete', summary: 'No completed review is available. See the PR summary.' } };
|
|
86
|
+
// Save interruption context before starting the remote check.
|
|
87
|
+
this.store.update(job.id, { report: JSON.stringify({ initial, body: '# atmin review — interrupted\n\nReview did not finish.', check }) });
|
|
88
|
+
await this.checks.publish(job, initial.headSha, { status: 'in_progress', output: { title: 'Review in progress', summary: `Reviewing head ${initial.headSha} against target ${initial.baseSha}.` } });
|
|
89
|
+
if (!this.store.current(job, this.owner) || signal.aborted)
|
|
90
|
+
return;
|
|
91
|
+
let artifact = null;
|
|
92
|
+
if (!this.store.reserve(job, this.owner, this.config.maxReviewsPerDay)) {
|
|
93
|
+
report = '# atmin review — review not run\n\nThe operator’s rolling 24-hour review limit was reached. A maintainer can rerun after capacity is available. No inference was started.';
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
artifact = join(this.config.stateDirectory, 'runs', job.id);
|
|
97
|
+
// A crash publishes this honest interruption report; it never starts another model request.
|
|
98
|
+
this.store.update(job.id, { artifact, report: JSON.stringify({ initial,
|
|
99
|
+
body: `# atmin review — interrupted\n\nThe worker stopped before it saved a validated report. No completed review is claimed. A maintainer can explicitly rerun.\n\nHead: \`${initial.headSha}\` · Target: \`${initial.baseSha}\` · Run: \`${job.id}\``,
|
|
100
|
+
}) });
|
|
101
|
+
try {
|
|
102
|
+
artifact = await this.run(job, signal);
|
|
103
|
+
if (!this.store.current(job, this.owner) || signal.aborted)
|
|
104
|
+
return;
|
|
105
|
+
const packet = parsePacket(JSON.parse(readFileSync(join(artifact, 'packet.json'), 'utf8')));
|
|
106
|
+
const result = parseResult(JSON.parse(readFileSync(join(artifact, 'result.json'), 'utf8')));
|
|
107
|
+
const live = await this.github.pull(job.pr);
|
|
108
|
+
if (packet.repository !== this.config.repository || packet.pr !== job.pr || compareCurrent(packet, live).status !== 'current' || live.draft || !same(initial, live)) {
|
|
109
|
+
this.cancel(job);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const assessment = assess(packet, result, compareCurrent(packet, live));
|
|
113
|
+
report = renderMarkdown(packet, result, assessment);
|
|
114
|
+
check = assessmentCheck(assessment);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
if (!this.store.current(job, this.owner) || signal.aborted)
|
|
118
|
+
return;
|
|
119
|
+
report = '# atmin review — review failed\n\nSource capture or investigation did not produce a validated report. No successful review or merge approval is claimed. A maintainer may request a new run with `/atmin review`; it uses a new budget reservation.';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
report += `\n\nHead: \`${initial.headSha}\` · Target: \`${initial.baseSha}\` · Run: \`${job.id}\`\n`;
|
|
123
|
+
// Persist the report and its exact publication identity before any write.
|
|
124
|
+
if (!this.store.current(job, this.owner))
|
|
125
|
+
return;
|
|
126
|
+
this.store.update(job.id, { state: 'publishing', artifact, report: JSON.stringify({ initial, body: report, check }) });
|
|
127
|
+
job = this.store.get(job.id);
|
|
128
|
+
}
|
|
129
|
+
const saved = JSON.parse(job.report);
|
|
130
|
+
if (!same(saved.initial, await this.github.pull(job.pr))) {
|
|
131
|
+
this.cancel(job);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Reconciliation refreshes CI and formatting from saved source evidence, without inference.
|
|
135
|
+
if (job.artifact && existsSync(join(job.artifact, 'packet.json')) && existsSync(join(job.artifact, 'result.json'))) {
|
|
136
|
+
const packet = parsePacket(JSON.parse(readFileSync(join(job.artifact, 'packet.json'), 'utf8')));
|
|
137
|
+
const result = parseResult(JSON.parse(readFileSync(join(job.artifact, 'result.json'), 'utf8')));
|
|
138
|
+
const live = await this.github.pull(job.pr);
|
|
139
|
+
if (packet.repository !== this.config.repository || packet.pr !== job.pr || !same(saved.initial, live)
|
|
140
|
+
|| compareCurrent(packet, live).status !== 'current') {
|
|
141
|
+
this.cancel(job);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const ci = await this.github.validation(packet.headSha, packet.policy.requiredChecks);
|
|
145
|
+
const assessment = assess(packet, result, compareCurrent(packet, live), ci);
|
|
146
|
+
if (!this.store.current(job, this.owner) || signal.aborted)
|
|
147
|
+
return;
|
|
148
|
+
writeFileSync(join(job.artifact, 'validation.json'), JSON.stringify({ headSha: packet.headSha, baseSha: packet.baseSha,
|
|
149
|
+
checkedAt: new Date().toISOString(), checks: ci }, null, 2), { mode: 0o600 });
|
|
150
|
+
saved.body = `${renderMarkdown(packet, result, assessment)}\nRun: \`${job.id}\`\n`;
|
|
151
|
+
saved.check = assessmentCheck(assessment);
|
|
152
|
+
this.store.update(job.id, { report: JSON.stringify(saved) });
|
|
153
|
+
}
|
|
154
|
+
const existing = await this.github.summary(job.pr, marker);
|
|
155
|
+
if (!this.store.current(job, this.owner) || signal.aborted)
|
|
156
|
+
return;
|
|
157
|
+
// Listing comments can take time; recheck commits immediately before publication.
|
|
158
|
+
if (!same(saved.initial, await this.github.pull(job.pr))) {
|
|
159
|
+
this.cancel(job);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!this.store.current(job, this.owner) || signal.aborted)
|
|
163
|
+
return;
|
|
164
|
+
const limited = saved.body.length > 58_000 ? `${saved.body.slice(0, 58_000)}\n\n**Report truncated for GitHub. Full evidence remains in the operator’s private run artifacts.**` : saved.body;
|
|
165
|
+
const body = `${marker}\n${limited}\n\nFreshness was checked immediately before publication at ${new Date().toISOString()}; GitHub writes are not atomic with PR updates.\n`;
|
|
166
|
+
let comment;
|
|
167
|
+
if (existing) {
|
|
168
|
+
this.store.creationConfirmed(job.pr, existing.id);
|
|
169
|
+
await this.github.update(existing.id, body);
|
|
170
|
+
comment = existing.id;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
if (this.store.uncertainCreate(job.pr)) {
|
|
174
|
+
this.store.update(job.id, { state: 'uncertain', error: 'earlier-create-uncertain; reconcile publication without repeating inference' });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.store.update(job.id, { createStarted: 1 });
|
|
178
|
+
try {
|
|
179
|
+
comment = await this.github.create(job.pr, body);
|
|
180
|
+
this.store.creationConfirmed(job.pr, comment);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
if (this.store.current(job, this.owner))
|
|
184
|
+
this.store.update(job.id, { state: 'uncertain', error: 'comment-create-uncertain; reconcile publication without repeating inference' });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const after = await this.github.pull(job.pr);
|
|
189
|
+
if (!this.store.current(job, this.owner) || !same(saved.initial, after)) {
|
|
190
|
+
// This worker owns publication until the request completes. Do not write after losing its lease.
|
|
191
|
+
if (this.store.owns(this.owner))
|
|
192
|
+
await this.github.update(comment, `${marker}\n# atmin review — superseded\n\nPR state changed during publication. Findings for head \`${saved.initial.headSha}\` are historical. Await a review of the current commits.`);
|
|
193
|
+
this.cancel(job);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
await this.checks.publish(job, saved.initial.headSha, { ...(saved.check ?? { status: 'completed', conclusion: 'failure', output: { title: 'Review incomplete', summary: 'Saved review has no passing check assessment. See the PR summary.' } }), details_url: `https://github.com/${this.config.repository}/pull/${job.pr}#issuecomment-${comment}` });
|
|
197
|
+
if (!this.store.current(job, this.owner) || !same(saved.initial, await this.github.pull(job.pr))) {
|
|
198
|
+
if (this.store.owns(this.owner))
|
|
199
|
+
await this.github.update(comment, `${marker}\n# atmin review — superseded\n\nPR state changed during check publication. Findings for head \`${saved.initial.headSha}\` are historical. Await a review of the current commits.`);
|
|
200
|
+
this.cancel(job);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.store.update(job.id, { state: 'completed', error: null });
|
|
204
|
+
}
|
|
205
|
+
cancel(job) {
|
|
206
|
+
if (this.store.owns(this.owner))
|
|
207
|
+
this.store.update(job.id, { state: 'cancelled', error: 'superseded' });
|
|
208
|
+
}
|
|
209
|
+
}
|