pierre-review 0.1.83 → 0.1.85
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/api/routes/activity.js +6 -0
- package/dist/api/routes/bot-triage.js +110 -10
- package/dist/api/routes/repos.js +73 -2
- package/dist/cli.js +151 -16
- package/dist/db/migrations/0033_thread_addressed_confidence.sql +9 -0
- package/dist/db/migrations/meta/_journal.json +7 -0
- package/dist/db/migrations-pg/0020_majestic_edwin_jarvis.sql +3 -0
- package/dist/db/migrations-pg/meta/0020_snapshot.json +3228 -0
- package/dist/db/migrations-pg/meta/_journal.json +8 -1
- package/dist/db/queries.js +524 -38
- package/dist/db/schema.pg.js +11 -0
- package/dist/db/schema.sqlite.js +12 -0
- package/dist/github/queries.js +65 -0
- package/dist/status.js +249 -0
- package/dist/sync/bot-resolution-markers.js +30 -0
- package/dist/sync/derive-thread-state.js +79 -18
- package/dist/sync/upsert.js +11 -1
- package/package.json +1 -1
- package/public/assets/{AiFixTab-BFClLWjp.js → AiFixTab-DVo7Cp_r.js} +1 -1
- package/public/assets/{ClaudeReviewTab-CD7NYRVF.js → ClaudeReviewTab-DLfa9u-P.js} +1 -1
- package/public/assets/index-DLN1zktY.css +1 -0
- package/public/assets/index-DNMKo7St.js +54 -0
- package/public/index.html +2 -2
- package/public/assets/index-CB6hpE0g.js +0 -53
- package/public/assets/index-DEYTjZtN.css +0 -1
|
@@ -27,6 +27,11 @@ export async function activityRoutes(app) {
|
|
|
27
27
|
const limit = q.limit != null ? Number(q.limit) : null;
|
|
28
28
|
const offset = q.offset != null ? Number(q.offset) : 0;
|
|
29
29
|
const prId = q.prId != null ? Number(q.prId) : null;
|
|
30
|
+
// Bot-only feed window (days) — clamped to 1..90; only honored on the botsOnly path.
|
|
31
|
+
const botWindowDaysRaw = q.botWindowDays != null ? Number(q.botWindowDays) : null;
|
|
32
|
+
const botWindowDays = botWindowDaysRaw != null && Number.isFinite(botWindowDaysRaw)
|
|
33
|
+
? Math.min(90, Math.max(1, Math.trunc(botWindowDaysRaw)))
|
|
34
|
+
: null;
|
|
30
35
|
return getConsolidatedFeed(accountIdOf(req), {
|
|
31
36
|
repoIds: parseIntList(q.repoIds),
|
|
32
37
|
userIds: parseIntList(q.userIds),
|
|
@@ -36,6 +41,7 @@ export async function activityRoutes(app) {
|
|
|
36
41
|
excludeBots: q.excludeBots === 'true',
|
|
37
42
|
allowBotIds: parseIntList(q.allowBotIds),
|
|
38
43
|
botsOnly: q.botsOnly === 'true',
|
|
44
|
+
botWindowDays,
|
|
39
45
|
});
|
|
40
46
|
});
|
|
41
47
|
// Mark the Activity Feed as seen (bumps the account's server-side "seen" marker to
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { addBotMuteRule, deleteBotMuteRule, getBotAnalytics, getBotVendorPrs, getBotDedupClusters, listBotMuteRules, listDetectedReviewers, resolveScopeRepoIds, setReviewerOverride, } from '../../db/queries.js';
|
|
1
|
+
import { addBotMuteRule, deleteBotMuteRule, getBotAnalytics, getBotOnlyPrs, getBotVendorPrs, getBotDedupClusters, getResolvableBotThreadPrs, getResolvableBotThreadsForScope, listBotMuteRules, listDetectedReviewers, resolveScopeRepoIds, setReviewerOverride, SCOPE_RESOLVE_THREAD_CAP, } from '../../db/queries.js';
|
|
2
|
+
import { resolveThreadsOnGitHub } from '../../bot-triage/resolve.js';
|
|
2
3
|
import { accountIdOf } from '../plugins/auth.js';
|
|
3
4
|
// Parse a comma-separated id list into a positive-int array, or null when empty/absent (so
|
|
4
5
|
// the query layer treats it as "all repos"). Mirrors the parser in activity.ts.
|
|
@@ -51,14 +52,14 @@ const analyticsSchema = {
|
|
|
51
52
|
},
|
|
52
53
|
},
|
|
53
54
|
};
|
|
54
|
-
// GET /api/bot-analytics/:
|
|
55
|
-
//
|
|
56
|
-
//
|
|
55
|
+
// GET /api/bot-analytics/vendor/:key/prs?window= — per-REVIEWER PR drill-down. `key` is the
|
|
56
|
+
// analytics-row identity: `u<userId>` (a single reviewer) or the 'pierre' sentinel; the handler
|
|
57
|
+
// parses it (anything else → 400). The window reuses the same closed enum/default as /api/bot-analytics.
|
|
57
58
|
const vendorPrsSchema = {
|
|
58
59
|
params: {
|
|
59
60
|
type: 'object',
|
|
60
|
-
required: ['
|
|
61
|
-
properties: {
|
|
61
|
+
required: ['key'],
|
|
62
|
+
properties: { key: { type: 'string' } },
|
|
62
63
|
},
|
|
63
64
|
querystring: {
|
|
64
65
|
type: 'object',
|
|
@@ -104,6 +105,39 @@ const ruleIdParamSchema = {
|
|
|
104
105
|
properties: { id: { type: 'integer' } },
|
|
105
106
|
},
|
|
106
107
|
};
|
|
108
|
+
// GET /api/bot-threads/resolvable?scope=&repoIds= — the scope-wide review list. Same
|
|
109
|
+
// scope/repoIds resolution as the analytics routes (an explicit `repoIds` wins over `scope`).
|
|
110
|
+
const resolvableSchema = {
|
|
111
|
+
querystring: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
additionalProperties: false,
|
|
114
|
+
properties: {
|
|
115
|
+
scope: { type: 'string' },
|
|
116
|
+
repoIds: { type: 'string' },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
// POST /api/bot-threads/resolve — the confirm-gated scope-wide resolve. `threadIds` is the
|
|
121
|
+
// explicit reviewed list (required, non-empty allowed to be [] → no-op), capped at
|
|
122
|
+
// SCOPE_RESOLVE_THREAD_CAP per request (the client chunks a larger selection); `repoIds` is the
|
|
123
|
+
// optional scope the server re-derives eligibility against. maxItems is a hard input guard.
|
|
124
|
+
const scopeResolveSchema = {
|
|
125
|
+
body: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
required: ['threadIds'],
|
|
128
|
+
additionalProperties: false,
|
|
129
|
+
properties: {
|
|
130
|
+
threadIds: {
|
|
131
|
+
type: 'array',
|
|
132
|
+
items: { type: 'integer' },
|
|
133
|
+
maxItems: SCOPE_RESOLVE_THREAD_CAP,
|
|
134
|
+
},
|
|
135
|
+
// Bounded like threadIds — an account has ≤100 repos, so 500 is generous headroom; an
|
|
136
|
+
// unbounded array would only ever be a hand-crafted request heading for a DB error.
|
|
137
|
+
repoIds: { type: 'array', items: { type: 'integer' }, maxItems: SCOPE_RESOLVE_THREAD_CAP },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
107
141
|
// Bot-triage platform routes (CORE, always registered). Detection/override, ROI analytics,
|
|
108
142
|
// per-PR cross-bot dedup, and the mute / auto-triage rule store. Every handler is account-
|
|
109
143
|
// scoped via accountIdOf(req); id-addressed reads/writes verify ownership → 404. No AI.
|
|
@@ -139,15 +173,40 @@ export async function botTriageRoutes(app) {
|
|
|
139
173
|
const resp = await getBotAnalytics(accountId, window, scopeRepoIds);
|
|
140
174
|
return resp;
|
|
141
175
|
});
|
|
142
|
-
// The
|
|
176
|
+
// The exact PR list behind the analytics totals.botOnlyPrs count — "only a bot reviewed these".
|
|
177
|
+
// Same window/scope resolution as /api/bot-analytics (a specific `repoIds` wins over `scope`) so
|
|
178
|
+
// the amber caption's number and this expandable list are computed identically and can't drift.
|
|
179
|
+
// Unbounded but small (real bot-only PRs); no pagination.
|
|
180
|
+
app.get('/api/bot-analytics/bot-only-prs', { schema: analyticsSchema }, async (req) => {
|
|
181
|
+
const { window, scope, repoIds } = req.query;
|
|
182
|
+
const accountId = accountIdOf(req);
|
|
183
|
+
const explicit = parseIntList(repoIds);
|
|
184
|
+
const scopeRepoIds = explicit ?? (scope ? await resolveScopeRepoIds(accountId, scope) : null);
|
|
185
|
+
const { window: win, prs } = await getBotOnlyPrs(accountId, window, scopeRepoIds);
|
|
186
|
+
const resp = { window: win, prs, generatedAt: new Date().toISOString() };
|
|
187
|
+
return resp;
|
|
188
|
+
});
|
|
189
|
+
// The per-REVIEWER PR drill-down behind one Bot-ROI row: the PRs that one automated reviewer
|
|
143
190
|
// touched in the window (threads/comments/acted-on/untouched/bot-only), newest-activity first.
|
|
144
|
-
|
|
145
|
-
|
|
191
|
+
// `key` is the analytics row identity — `u<userId>` (a single reviewer) or the 'pierre' sentinel;
|
|
192
|
+
// anything else is a client bug → 400.
|
|
193
|
+
app.get('/api/bot-analytics/vendor/:key/prs', { schema: vendorPrsSchema }, async (req, reply) => {
|
|
194
|
+
const { key } = req.params;
|
|
146
195
|
const { window, scope, repoIds } = req.query;
|
|
196
|
+
const m = /^u(\d+)$/.exec(key);
|
|
197
|
+
const target = m
|
|
198
|
+
? { userId: Number(m[1]) }
|
|
199
|
+
: key === 'pierre'
|
|
200
|
+
? { kind: 'pierre' }
|
|
201
|
+
: null;
|
|
202
|
+
if (!target) {
|
|
203
|
+
reply.status(400);
|
|
204
|
+
return { error: 'BadRequest', message: `Invalid vendor key: ${key}` };
|
|
205
|
+
}
|
|
147
206
|
const accountId = accountIdOf(req);
|
|
148
207
|
const explicit = parseIntList(repoIds);
|
|
149
208
|
const scopeRepoIds = explicit ?? (scope ? await resolveScopeRepoIds(accountId, scope) : null);
|
|
150
|
-
const resp = await getBotVendorPrs(accountId,
|
|
209
|
+
const resp = await getBotVendorPrs(accountId, target, window, scopeRepoIds);
|
|
151
210
|
return resp;
|
|
152
211
|
});
|
|
153
212
|
// Cross-bot dedup clusters for one PR (≥2 automated reviewers on the same path/line window).
|
|
@@ -182,5 +241,46 @@ export async function botTriageRoutes(app) {
|
|
|
182
241
|
reply.status(204);
|
|
183
242
|
return null;
|
|
184
243
|
});
|
|
244
|
+
// The scope-wide review list: every PR with ≥1 `likely_addressed` automated-reviewer thread
|
|
245
|
+
// across the account (or a repo scope), UNCAPPED, newest-thread-first, each row carrying all
|
|
246
|
+
// its resolvable thread ids + a bot thread-state mix + `totalThreads` (the whole backlog). The
|
|
247
|
+
// client sorts / paginates / "Select all"s across pages and chunks the resolve. Read-only.
|
|
248
|
+
app.get('/api/bot-threads/resolvable', { schema: resolvableSchema }, async (req) => {
|
|
249
|
+
const { scope, repoIds } = req.query;
|
|
250
|
+
const accountId = accountIdOf(req);
|
|
251
|
+
const explicit = parseIntList(repoIds);
|
|
252
|
+
const scopeRepoIds = explicit ?? (scope ? await resolveScopeRepoIds(accountId, scope) : null);
|
|
253
|
+
const { prs, totalThreads } = await getResolvableBotThreadPrs(accountId, scopeRepoIds);
|
|
254
|
+
const resp = {
|
|
255
|
+
prs,
|
|
256
|
+
totalThreads,
|
|
257
|
+
generatedAt: new Date().toISOString(),
|
|
258
|
+
};
|
|
259
|
+
return resp;
|
|
260
|
+
});
|
|
261
|
+
// The confirm-gated scope-wide resolve. NEVER blind: the server RE-DERIVES the eligible set
|
|
262
|
+
// (owned + automated-reviewer-originated + `likely_addressed` + unresolved) ∩ the client's
|
|
263
|
+
// explicit reviewed ids, scoped to `repoIds`, then resolves each via the SAME shared helper the
|
|
264
|
+
// per-PR route + the standing auto-triage job use. An empty list is a no-op (not an error);
|
|
265
|
+
// per-thread failures are reported, not fatal. The re-derive path passes `threadIds` so the
|
|
266
|
+
// page cap is bypassed — no requested-and-eligible id is silently dropped.
|
|
267
|
+
app.post('/api/bot-threads/resolve', { schema: scopeResolveSchema }, async (req) => {
|
|
268
|
+
const { threadIds, repoIds } = req.body;
|
|
269
|
+
const accountId = accountIdOf(req);
|
|
270
|
+
if (threadIds.length === 0) {
|
|
271
|
+
const noop = { resolved: 0, failed: 0, results: [] };
|
|
272
|
+
return noop;
|
|
273
|
+
}
|
|
274
|
+
const { threads: eligible } = await getResolvableBotThreadsForScope(accountId, repoIds ?? null, threadIds);
|
|
275
|
+
const result = await resolveThreadsOnGitHub(accountId, eligible.map((t) => ({ id: t.threadId, threadNodeId: t.threadNodeId })));
|
|
276
|
+
req.log.info({
|
|
277
|
+
accountId,
|
|
278
|
+
requested: threadIds.length,
|
|
279
|
+
eligible: eligible.length,
|
|
280
|
+
resolved: result.resolved,
|
|
281
|
+
failed: result.failed,
|
|
282
|
+
}, 'scope-wide bot-thread resolve');
|
|
283
|
+
return result;
|
|
284
|
+
});
|
|
185
285
|
}
|
|
186
286
|
//# sourceMappingURL=bot-triage.js.map
|
package/dist/api/routes/repos.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getGraphqlClientFor } from '../../github/client.js';
|
|
1
|
+
import { getGraphqlClientFor, graphqlTolerant } from '../../github/client.js';
|
|
2
2
|
import { getAccessToken } from '../../auth/account.js';
|
|
3
|
-
import { OWNER_TYPE_QUERY, REPO_ID_QUERY, REPO_SEARCH_QUERY, } from '../../github/queries.js';
|
|
3
|
+
import { OWNER_TYPE_QUERY, REPO_ID_QUERY, REPO_SEARCH_QUERY, VIEWER_REPOS_QUERY, } from '../../github/queries.js';
|
|
4
4
|
import { upsertRepo } from '../../sync/upsert.js';
|
|
5
5
|
import { getSyncStatus, isSyncRunning, requestSyncCancel, runSyncForRepo, waitForSyncToStop, } from '../../sync/sync-manager.js';
|
|
6
6
|
import { deleteRepo, getRepo, getWatchedRepoNodeIds, listRepos, setRepoInboxWatch, } from '../../db/queries.js';
|
|
@@ -188,6 +188,77 @@ export async function repoRoutes(app) {
|
|
|
188
188
|
};
|
|
189
189
|
return body;
|
|
190
190
|
});
|
|
191
|
+
// First-run onboarding: the viewer's recently-active repositories, detected from their
|
|
192
|
+
// GitHub activity (recent pushes + contributions). Same token idiom as /search; already-
|
|
193
|
+
// watched repos filtered out; mapped to the picker's RepoSearchResult shape and ordered
|
|
194
|
+
// most-recently-pushed first, capped at 30. Detail comes straight from GitHub — nothing
|
|
195
|
+
// persisted. Local's broad `gh` token surfaces private + org repos; a scoped cloud token
|
|
196
|
+
// surfaces only what it can read (both are handled by the same null-tolerant merge below).
|
|
197
|
+
app.get('/api/repos/suggested', async (req, reply) => {
|
|
198
|
+
const accountId = accountIdOf(req);
|
|
199
|
+
const client = getGraphqlClientFor(await getAccessToken(accountId));
|
|
200
|
+
let resp;
|
|
201
|
+
try {
|
|
202
|
+
// Tolerate PARTIAL GraphQL errors (a scoped cloud token forbidden one sub-field answers
|
|
203
|
+
// 200 + partial data) — the null-drop merge below is built for exactly that. Only a
|
|
204
|
+
// response with NO usable data (auth failure, rate limit, network) 502s.
|
|
205
|
+
resp = await graphqlTolerant(client, VIEWER_REPOS_QUERY, {}, (errors) => req.log.warn({ errors }, 'partial viewer-repos response'));
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
reply.status(502);
|
|
209
|
+
return {
|
|
210
|
+
error: 'GitHubError',
|
|
211
|
+
message: err instanceof Error ? err.message : 'GitHub request failed',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (resp?.viewer == null)
|
|
215
|
+
return { results: [] };
|
|
216
|
+
const me = resp.viewer.login.toLowerCase();
|
|
217
|
+
const orgLogins = new Set(resp.viewer.organizations.nodes
|
|
218
|
+
.filter((o) => o != null)
|
|
219
|
+
.map((o) => o.login.toLowerCase()));
|
|
220
|
+
// Merge the two recency-ordered lists, deduping by repo node id and keeping the more
|
|
221
|
+
// recent pushedAt. Null node arrays (scoped token) and null nodes are dropped BEFORE any
|
|
222
|
+
// field read (ISO-8601 pushedAt strings compare lexically; a null pushedAt sorts last).
|
|
223
|
+
const byId = new Map();
|
|
224
|
+
const merged = [
|
|
225
|
+
...(resp.viewer.repositories.nodes ?? []),
|
|
226
|
+
...(resp.viewer.repositoriesContributedTo.nodes ?? []),
|
|
227
|
+
];
|
|
228
|
+
for (const n of merged) {
|
|
229
|
+
if (n == null || typeof n.id !== 'string')
|
|
230
|
+
continue;
|
|
231
|
+
const existing = byId.get(n.id);
|
|
232
|
+
if (existing == null || (n.pushedAt ?? '') > (existing.pushedAt ?? '')) {
|
|
233
|
+
byId.set(n.id, n);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const watched = await getWatchedRepoNodeIds(accountId);
|
|
237
|
+
const results = [...byId.values()]
|
|
238
|
+
.filter((n) => !watched.has(n.id))
|
|
239
|
+
// Most-recently-pushed first; a missing pushedAt sorts to the bottom.
|
|
240
|
+
.sort((a, b) => (b.pushedAt ?? '').localeCompare(a.pushedAt ?? ''))
|
|
241
|
+
.slice(0, 30)
|
|
242
|
+
.map((n) => {
|
|
243
|
+
const ownerLogin = n.owner.login;
|
|
244
|
+
return {
|
|
245
|
+
githubNodeId: n.id,
|
|
246
|
+
owner: ownerLogin,
|
|
247
|
+
name: n.name,
|
|
248
|
+
fullName: n.nameWithOwner,
|
|
249
|
+
description: n.description,
|
|
250
|
+
ownerAvatarUrl: n.owner.avatarUrl,
|
|
251
|
+
stargazerCount: n.stargazerCount,
|
|
252
|
+
openPrCount: n.pullRequests.totalCount,
|
|
253
|
+
url: n.url,
|
|
254
|
+
isPrivate: n.isPrivate,
|
|
255
|
+
isOwnedOrMember: ownerLogin.toLowerCase() === me ||
|
|
256
|
+
orgLogins.has(ownerLogin.toLowerCase()),
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
const body = { results };
|
|
260
|
+
return body;
|
|
261
|
+
});
|
|
191
262
|
app.post('/api/repos', { schema: createRepoSchema }, async (req, reply) => {
|
|
192
263
|
const { owner, name } = req.body;
|
|
193
264
|
const accountId = accountIdOf(req);
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile, execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
// ── tiny ANSI helpers (degrade gracefully when not a TTY) ──────────────────
|
|
7
7
|
const useColor = process.stdout.isTTY && process.env.NO_COLOR === undefined;
|
|
8
8
|
const paint = (code, s) => useColor ? `[${code}m${s}[0m` : s;
|
|
@@ -66,6 +66,11 @@ function printUsage() {
|
|
|
66
66
|
Usage:
|
|
67
67
|
pierre [options]
|
|
68
68
|
pierre-review [options]
|
|
69
|
+
pierre status [options]
|
|
70
|
+
|
|
71
|
+
Commands:
|
|
72
|
+
status Your cross-repo "my turn" queue in the terminal, with
|
|
73
|
+
clickable links (see \`pierre status --help\`)
|
|
69
74
|
|
|
70
75
|
Options:
|
|
71
76
|
--no-open Don't open the browser (also honours NO_OPEN env)
|
|
@@ -98,17 +103,157 @@ function openBrowser(url) {
|
|
|
98
103
|
/* ignore */
|
|
99
104
|
});
|
|
100
105
|
}
|
|
106
|
+
// The default local SQLite location — a user-writable home dir, never the (possibly
|
|
107
|
+
// read-only) install dir. Shared by the server boot and `pierre status`.
|
|
108
|
+
function defaultLocalDbPath() {
|
|
109
|
+
return join(homedir(), '.pierre-review', 'pierre-review.sqlite');
|
|
110
|
+
}
|
|
111
|
+
// Fail fast with a friendly message when the GitHub CLI isn't installed / authed.
|
|
112
|
+
// Both the local server boot and `pierre status` need a working `gh` token.
|
|
113
|
+
function ghAuthPreCheck() {
|
|
114
|
+
try {
|
|
115
|
+
execFileSync('gh', ['auth', 'token'], { stdio: 'ignore' });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
console.error('');
|
|
119
|
+
console.error(bold('GitHub CLI not found or not authenticated.'));
|
|
120
|
+
console.error('Install the GitHub CLI (https://cli.github.com) and run `gh auth login`.');
|
|
121
|
+
console.error('');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function parseStatusArgs(argv) {
|
|
126
|
+
const opts = {
|
|
127
|
+
watch: false,
|
|
128
|
+
sync: false,
|
|
129
|
+
intervalSeconds: 60,
|
|
130
|
+
help: false,
|
|
131
|
+
};
|
|
132
|
+
for (let i = 0; i < argv.length; i++) {
|
|
133
|
+
const arg = argv[i];
|
|
134
|
+
switch (arg) {
|
|
135
|
+
case '--watch':
|
|
136
|
+
opts.watch = true;
|
|
137
|
+
break;
|
|
138
|
+
case '--sync':
|
|
139
|
+
opts.sync = true;
|
|
140
|
+
break;
|
|
141
|
+
case '--interval': {
|
|
142
|
+
const v = argv[++i];
|
|
143
|
+
const n = v ? Number.parseInt(v, 10) : NaN;
|
|
144
|
+
if (!Number.isFinite(n)) {
|
|
145
|
+
console.error('--interval requires a number of seconds');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
opts.intervalSeconds = Math.max(10, n);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case '--db': {
|
|
152
|
+
const v = argv[++i];
|
|
153
|
+
if (!v) {
|
|
154
|
+
console.error('--db requires a path');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
opts.db = v;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case '--help':
|
|
161
|
+
case '-h':
|
|
162
|
+
opts.help = true;
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
console.error(`Unknown argument: ${arg}`);
|
|
166
|
+
console.error('Run `pierre status --help` for usage.');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return opts;
|
|
171
|
+
}
|
|
172
|
+
function printStatusUsage() {
|
|
173
|
+
console.log(`pierre status — your cross-repo "my turn" queue in the terminal
|
|
174
|
+
|
|
175
|
+
Usage:
|
|
176
|
+
pierre status [options]
|
|
177
|
+
|
|
178
|
+
Options:
|
|
179
|
+
--sync Fetch fresh state from GitHub first (otherwise shows data as of
|
|
180
|
+
the last sync). Under --watch, re-syncs at most every 5 minutes.
|
|
181
|
+
--watch Repaint on an interval until you quit (Ctrl-C)
|
|
182
|
+
--interval <n> Seconds between repaints under --watch (default 60, min 10)
|
|
183
|
+
--db <path> SQLite DB path (also DATABASE_URL env)
|
|
184
|
+
-h, --help Show this help
|
|
185
|
+
|
|
186
|
+
Reads the local pierre database. Without --sync it shows data as of the last sync
|
|
187
|
+
— run it alongside \`pierre\`, or pass --sync to fetch fresh state first. Links are
|
|
188
|
+
clickable in terminals that support OSC-8 hyperlinks.
|
|
189
|
+
`);
|
|
190
|
+
}
|
|
191
|
+
async function runStatusCommand(argv) {
|
|
192
|
+
const opts = parseStatusArgs(argv);
|
|
193
|
+
if (opts.help) {
|
|
194
|
+
printStatusUsage();
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
// status is LOCAL-only — it reads your on-disk SQLite database directly.
|
|
198
|
+
if (process.env.DEPLOYMENT_MODE === 'cloud') {
|
|
199
|
+
console.error('`pierre status` is local-only — it reads your local SQLite database.');
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
// Pin local BEFORE the dynamic import: config.ts loads .env files on import, and
|
|
203
|
+
// process.loadEnvFile never overrides an already-set var — so an .env-declared
|
|
204
|
+
// DEPLOYMENT_MODE=cloud can't flip status onto the Postgres driver after the guard.
|
|
205
|
+
process.env.DEPLOYMENT_MODE = 'local';
|
|
206
|
+
// ── map flags → env BEFORE any dynamic import (config/db snapshot env on load) ──
|
|
207
|
+
// resolve() the --db flag against the CWD (what the user means by a relative path);
|
|
208
|
+
// config.ts resolves relative paths against the INSTALL dir, so an unresolved value
|
|
209
|
+
// would make this guard check one file while the DB opens another.
|
|
210
|
+
process.env.NODE_ENV ??= 'production';
|
|
211
|
+
if (opts.db !== undefined)
|
|
212
|
+
process.env.DATABASE_URL = resolve(opts.db);
|
|
213
|
+
if (!process.env.DATABASE_URL)
|
|
214
|
+
process.env.DATABASE_URL = defaultLocalDbPath();
|
|
215
|
+
try {
|
|
216
|
+
mkdirSync(dirname(process.env.DATABASE_URL), { recursive: true });
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
/* best-effort; client.ts also mkdirs */
|
|
220
|
+
}
|
|
221
|
+
// Friendlier than the server path: don't silently create an empty DB just to show
|
|
222
|
+
// an empty queue. --sync legitimately bootstraps a fresh DB, so skip the guard then.
|
|
223
|
+
if (!opts.sync && !existsSync(process.env.DATABASE_URL)) {
|
|
224
|
+
console.error(`No pierre database found at ${process.env.DATABASE_URL} — run \`pierre\` first to sync, or pass --db.`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
// ensureLocalAccount (`gh api user`) and --sync both need a working gh token.
|
|
228
|
+
ghAuthPreCheck();
|
|
229
|
+
const { runStatus } = await import('./status.js');
|
|
230
|
+
await runStatus({
|
|
231
|
+
watch: opts.watch,
|
|
232
|
+
sync: opts.sync,
|
|
233
|
+
intervalSeconds: opts.intervalSeconds,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
101
236
|
async function main() {
|
|
102
|
-
const
|
|
237
|
+
const argv = process.argv.slice(2);
|
|
238
|
+
// `pierre status` — a read-only subcommand with its own parser, usage, and env
|
|
239
|
+
// mapping. Peeled off BEFORE parseArgs (whose default case rejects any bare token),
|
|
240
|
+
// and it never reaches the server boot below.
|
|
241
|
+
if (argv[0] === 'status') {
|
|
242
|
+
await runStatusCommand(argv.slice(1));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const opts = parseArgs(argv);
|
|
103
246
|
if (opts.help) {
|
|
104
247
|
printUsage();
|
|
105
248
|
process.exit(0);
|
|
106
249
|
}
|
|
107
250
|
// ── map flags → env BEFORE importing config/app (config reads these) ──────
|
|
251
|
+
// --db is resolved against the CWD (config.ts resolves relative paths against the
|
|
252
|
+
// install dir, which is never what a user typing a relative path means).
|
|
108
253
|
if (opts.port !== undefined)
|
|
109
254
|
process.env.PORT = String(opts.port);
|
|
110
255
|
if (opts.db !== undefined)
|
|
111
|
-
process.env.DATABASE_URL = opts.db;
|
|
256
|
+
process.env.DATABASE_URL = resolve(opts.db);
|
|
112
257
|
if (opts.open === false)
|
|
113
258
|
process.env.NO_OPEN = '1';
|
|
114
259
|
if (opts.cloud)
|
|
@@ -120,8 +265,7 @@ async function main() {
|
|
|
120
265
|
// ── local: default the DB to a user-writable home location (mkdir -p) ────
|
|
121
266
|
// The package dir is read-only for global installs, so never write there.
|
|
122
267
|
if (!process.env.DATABASE_URL) {
|
|
123
|
-
|
|
124
|
-
process.env.DATABASE_URL = dbPath;
|
|
268
|
+
process.env.DATABASE_URL = defaultLocalDbPath();
|
|
125
269
|
}
|
|
126
270
|
try {
|
|
127
271
|
mkdirSync(dirname(process.env.DATABASE_URL), { recursive: true });
|
|
@@ -130,16 +274,7 @@ async function main() {
|
|
|
130
274
|
/* best-effort; client.ts also mkdirs */
|
|
131
275
|
}
|
|
132
276
|
// ── pre-check gh auth with a friendly message (contract §6) ─────────────
|
|
133
|
-
|
|
134
|
-
execFileSync('gh', ['auth', 'token'], { stdio: 'ignore' });
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
console.error('');
|
|
138
|
-
console.error(bold('GitHub CLI not found or not authenticated.'));
|
|
139
|
-
console.error('Install the GitHub CLI (https://cli.github.com) and run `gh auth login`.');
|
|
140
|
-
console.error('');
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
277
|
+
ghAuthPreCheck();
|
|
143
278
|
}
|
|
144
279
|
// Cloud mode: no gh pre-check (each account brings its own OAuth token) and no
|
|
145
280
|
// SQLite home default (DATABASE_URL must be a Postgres URL). config.ts /
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Part A: deterministic "was this thread addressed?" upgrade. Store a graded addressed-confidence
|
|
2
|
+
-- + a machine reason tag + who resolved the thread, alongside the existing derivedState. All
|
|
3
|
+
-- additive (defaulted / nullable) so the backfill is a no-op — the next sync recomputes them.
|
|
4
|
+
-- The Postgres baseline is regenerated separately via `pnpm db:generate:pg`.
|
|
5
|
+
ALTER TABLE `review_threads` ADD `addressed_confidence` text DEFAULT 'none' NOT NULL;
|
|
6
|
+
--> statement-breakpoint
|
|
7
|
+
ALTER TABLE `review_threads` ADD `addressed_reason` text;
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
ALTER TABLE `review_threads` ADD `resolved_by_login` text;
|
|
@@ -232,6 +232,13 @@
|
|
|
232
232
|
"when": 1783820880005,
|
|
233
233
|
"tag": "0032_benchmark_contributions",
|
|
234
234
|
"breakpoints": true
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"idx": 33,
|
|
238
|
+
"version": "6",
|
|
239
|
+
"when": 1784200000000,
|
|
240
|
+
"tag": "0033_thread_addressed_confidence",
|
|
241
|
+
"breakpoints": true
|
|
235
242
|
}
|
|
236
243
|
]
|
|
237
244
|
}
|