edsger 0.73.0 → 0.75.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.
Files changed (47) hide show
  1. package/dist/api/adr.d.ts +48 -0
  2. package/dist/api/adr.js +139 -0
  3. package/dist/commands/adr/index.d.ts +13 -0
  4. package/dist/commands/adr/index.js +31 -0
  5. package/dist/commands/features/index.d.ts +15 -0
  6. package/dist/commands/features/index.js +34 -0
  7. package/dist/commands/pr-resolve/index.d.ts +3 -1
  8. package/dist/commands/pr-resolve/index.js +12 -7
  9. package/dist/commands/pr-review/index.d.ts +3 -1
  10. package/dist/commands/pr-review/index.js +10 -6
  11. package/dist/commands/sync-github-pull-requests/index.d.ts +11 -0
  12. package/dist/commands/sync-github-pull-requests/index.js +42 -0
  13. package/dist/index.js +66 -4
  14. package/dist/phases/adr-generation/agent.d.ts +6 -0
  15. package/dist/phases/adr-generation/agent.js +69 -0
  16. package/dist/phases/adr-generation/index.d.ts +15 -0
  17. package/dist/phases/adr-generation/index.js +66 -0
  18. package/dist/phases/adr-generation/parse.d.ts +12 -0
  19. package/dist/phases/adr-generation/parse.js +123 -0
  20. package/dist/phases/adr-generation/prompts.d.ts +8 -0
  21. package/dist/phases/adr-generation/prompts.js +35 -0
  22. package/dist/phases/data-flow/mcp-server.d.ts +1 -1
  23. package/dist/phases/features/index.d.ts +65 -0
  24. package/dist/phases/features/index.js +292 -0
  25. package/dist/phases/features/mcp-server.d.ts +61 -0
  26. package/dist/phases/features/mcp-server.js +165 -0
  27. package/dist/phases/features/prompts.d.ts +32 -0
  28. package/dist/phases/features/prompts.js +92 -0
  29. package/dist/phases/features/types.d.ts +34 -0
  30. package/dist/phases/features/types.js +15 -0
  31. package/dist/phases/pr-resolve/index.d.ts +3 -1
  32. package/dist/phases/pr-resolve/index.js +12 -12
  33. package/dist/phases/pr-review/index.d.ts +3 -1
  34. package/dist/phases/pr-review/index.js +13 -16
  35. package/dist/phases/pr-shared/status.d.ts +18 -0
  36. package/dist/phases/pr-shared/status.js +37 -0
  37. package/dist/phases/quality-benchmark/parsers.js +79 -0
  38. package/dist/phases/quality-benchmark/rubric.md +125 -17
  39. package/dist/phases/quality-benchmark/tool-catalog.js +39 -0
  40. package/dist/phases/sync-github-pull-requests/index.d.ts +23 -0
  41. package/dist/phases/sync-github-pull-requests/index.js +210 -0
  42. package/dist/phases/sync-github-pull-requests/state.d.ts +24 -0
  43. package/dist/phases/sync-github-pull-requests/state.js +16 -0
  44. package/dist/phases/sync-github-pull-requests/types.d.ts +22 -0
  45. package/dist/phases/sync-github-pull-requests/types.js +1 -0
  46. package/dist/skills/phase/adr-generation/SKILL.md +51 -0
  47. package/package.json +1 -1
@@ -0,0 +1,210 @@
1
+ /**
2
+ * sync-github-pull-requests phase: pull every pull request from a repository's
3
+ * connected GitHub repo and mirror them into the `pull_requests` table scoped
4
+ * by `repository_id`. Deterministic — no Claude Agent SDK; plain Octokit plus
5
+ * the user's RLS-scoped Supabase session.
6
+ *
7
+ * Idempotent: rows are keyed on (repository_id, pull_request_number). New PRs
8
+ * are inserted, and the GitHub-owned fields (status/title/description) of
9
+ * already-synced PRs are refreshed. Re-running never duplicates a PR.
10
+ */
11
+ import { retry } from '@octokit/plugin-retry';
12
+ import { throttling } from '@octokit/plugin-throttling';
13
+ import { Octokit } from '@octokit/rest';
14
+ import { ensureSupabaseSession, getSupabase } from '../../supabase/client.js';
15
+ import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
16
+ import { acquireSyncGithubPullRequestsLock, updateSyncGithubPullRequestsState, } from './state.js';
17
+ // The plugins use `@octokit/core` 7.x types, while @octokit/rest@20 hoists
18
+ // to core 5.x at the workspace root. Runtime is fine; the cast tells TS to
19
+ // accept the plugin chain (same pattern as sync-github-issues).
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- octokit version drift across the workspace
21
+ const ResilientOctokit = Octokit.plugin(retry, throttling);
22
+ /** GitHub list-pulls max page size. */
23
+ const PER_PAGE = 100;
24
+ /** Build an Octokit instance with retry + throttling configured for sync. */
25
+ function buildOctokit(token) {
26
+ return new ResilientOctokit({
27
+ auth: token,
28
+ retry: { doNotRetry: [400, 401, 403, 404, 422] },
29
+ throttle: {
30
+ onRateLimit: (retryAfter, options) => {
31
+ logWarning(`GitHub rate limit hit on ${options.method ?? '?'} ${options.url ?? '?'} — retry in ${retryAfter}s`);
32
+ return (options.request?.retryCount ?? 0) < 2;
33
+ },
34
+ onSecondaryRateLimit: (retryAfter, options) => {
35
+ logWarning(`GitHub secondary rate limit on ${options.method ?? '?'} ${options.url ?? '?'} — retry in ${retryAfter}s`);
36
+ return true;
37
+ },
38
+ },
39
+ });
40
+ }
41
+ /** Pull every pull request from the repo, paginated (state=all). */
42
+ export async function fetchAllRepoPullRequests(octokit, owner, repo) {
43
+ const all = [];
44
+ let page = 1;
45
+ for (;;) {
46
+ const { data } = await octokit.request('GET /repos/{owner}/{repo}/pulls', {
47
+ owner,
48
+ repo,
49
+ state: 'all',
50
+ per_page: PER_PAGE,
51
+ page,
52
+ sort: 'created',
53
+ direction: 'asc',
54
+ });
55
+ if (data.length === 0) {
56
+ break;
57
+ }
58
+ all.push(...data);
59
+ if (data.length < PER_PAGE) {
60
+ break;
61
+ }
62
+ page += 1;
63
+ }
64
+ return all;
65
+ }
66
+ /** Map a GitHub PR's open/closed/merged state to our PR status enum. */
67
+ function toStatus(pr) {
68
+ if (pr.merged_at) {
69
+ return 'merged';
70
+ }
71
+ if (pr.state === 'closed') {
72
+ return 'closed';
73
+ }
74
+ return 'pr_opened';
75
+ }
76
+ export async function syncGithubPullRequests(options) {
77
+ const { repositoryId, githubToken, owner, repo, verbose } = options;
78
+ // Guard against two concurrent syncs of the same repo racing into duplicate
79
+ // inserts (the unique index would reject the loser, but failing fast is
80
+ // friendlier than surfacing a constraint error).
81
+ const lock = acquireSyncGithubPullRequestsLock(repositoryId);
82
+ if (!lock) {
83
+ return {
84
+ status: 'error',
85
+ message: 'Another GitHub pull request sync is already running for this repository',
86
+ };
87
+ }
88
+ try {
89
+ updateSyncGithubPullRequestsState(repositoryId, {
90
+ lastAttemptedAt: new Date().toISOString(),
91
+ });
92
+ const ready = await ensureSupabaseSession();
93
+ if (!ready) {
94
+ return {
95
+ status: 'error',
96
+ message: 'Supabase session unavailable. Sign in to the Edsger desktop app to authorize the CLI.',
97
+ };
98
+ }
99
+ const supabase = getSupabase();
100
+ logInfo(`Syncing GitHub pull requests for ${owner}/${repo} → repository ${repositoryId}`);
101
+ const octokit = buildOctokit(githubToken);
102
+ const remotePRs = await fetchAllRepoPullRequests(octokit, owner, repo);
103
+ logInfo(`Fetched ${remotePRs.length} pull requests from GitHub`);
104
+ // Existing repo-scoped PRs, keyed by GitHub PR number, so we never create
105
+ // a duplicate of one that was synced in a previous run.
106
+ const { data: existingRows, error: existingError } = await supabase
107
+ .from('pull_requests')
108
+ .select('id, pull_request_number, status, name, description')
109
+ .eq('repository_id', repositoryId);
110
+ if (existingError) {
111
+ throw new Error(`Failed to read existing pull requests: ${existingError.message}`);
112
+ }
113
+ const existingByNumber = new Map();
114
+ for (const row of existingRows ?? []) {
115
+ if (row.pull_request_number !== null) {
116
+ existingByNumber.set(row.pull_request_number, {
117
+ id: row.id,
118
+ status: row.status,
119
+ name: row.name,
120
+ description: row.description,
121
+ });
122
+ }
123
+ }
124
+ const toInsert = [];
125
+ const toUpdate = [];
126
+ for (const pr of remotePRs) {
127
+ const status = toStatus(pr);
128
+ const existing = existingByNumber.get(pr.number);
129
+ if (!existing) {
130
+ toInsert.push({
131
+ repository_id: repositoryId,
132
+ sequence: pr.number,
133
+ name: pr.title,
134
+ description: pr.body ?? null,
135
+ branch_name: pr.head?.ref ?? null,
136
+ pull_request_url: pr.html_url,
137
+ pull_request_number: pr.number,
138
+ status,
139
+ });
140
+ continue;
141
+ }
142
+ // Refresh GitHub-owned fields when they drift (e.g. PR got merged/closed).
143
+ const patch = {};
144
+ if (existing.status !== status) {
145
+ patch.status = status;
146
+ }
147
+ if (existing.name !== pr.title) {
148
+ patch.name = pr.title;
149
+ }
150
+ if ((existing.description ?? null) !== (pr.body ?? null)) {
151
+ patch.description = pr.body ?? null;
152
+ }
153
+ if (Object.keys(patch).length > 0) {
154
+ toUpdate.push({ id: existing.id, patch });
155
+ }
156
+ }
157
+ let createdCount = 0;
158
+ if (toInsert.length > 0) {
159
+ const { error: insertError } = await supabase
160
+ .from('pull_requests')
161
+ .insert(toInsert);
162
+ if (insertError) {
163
+ throw new Error(`Failed to insert pull requests: ${insertError.message}`);
164
+ }
165
+ createdCount = toInsert.length;
166
+ if (verbose) {
167
+ logInfo(`Inserted ${createdCount} new pull requests`);
168
+ }
169
+ }
170
+ let updatedCount = 0;
171
+ for (const { id, patch } of toUpdate) {
172
+ const { error: updateError } = await supabase
173
+ .from('pull_requests')
174
+ .update({ ...patch, updated_at: new Date().toISOString() })
175
+ .eq('id', id);
176
+ if (updateError) {
177
+ logWarning(`Failed to refresh pull request ${id}: ${updateError.message}`);
178
+ continue;
179
+ }
180
+ updatedCount++;
181
+ }
182
+ const summary = `fetched=${remotePRs.length} created=${createdCount} updated=${updatedCount}`;
183
+ logSuccess(`GitHub PR sync complete: ${summary}`);
184
+ updateSyncGithubPullRequestsState(repositoryId, {
185
+ lastSyncedAt: new Date().toISOString(),
186
+ lastRepoFullName: `${owner}/${repo}`,
187
+ lastError: undefined,
188
+ });
189
+ return {
190
+ status: 'success',
191
+ message: summary,
192
+ repository: `${owner}/${repo}`,
193
+ fetchedCount: remotePRs.length,
194
+ createdCount,
195
+ updatedCount,
196
+ };
197
+ }
198
+ catch (error) {
199
+ const errorMessage = error instanceof Error ? error.message : String(error);
200
+ logError(`GitHub PR sync failed: ${errorMessage}`);
201
+ updateSyncGithubPullRequestsState(repositoryId, { lastError: errorMessage });
202
+ return {
203
+ status: 'error',
204
+ message: `GitHub PR sync failed: ${errorMessage}`,
205
+ };
206
+ }
207
+ finally {
208
+ lock.release();
209
+ }
210
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Per-repository sync state for sync-github-pull-requests. Stored at
3
+ * `~/.edsger/sync-github-pull-requests-state/<repositoryId>.json`.
4
+ *
5
+ * The lock guards against two concurrent syncs of the same repository racing
6
+ * each other into duplicate inserts. State fields track the last run for
7
+ * diagnostics and a future incremental-sync optimisation.
8
+ */
9
+ import { type LockHandle } from '../find-shared/scan-state.js';
10
+ export type { LockHandle };
11
+ export interface SyncGithubPullRequestsState {
12
+ /** ISO timestamp of the last successful sync. */
13
+ lastSyncedAt?: string;
14
+ /** ISO timestamp we last attempted (success or fail). */
15
+ lastAttemptedAt?: string;
16
+ /** Last error message, if the most recent run failed. */
17
+ lastError?: string;
18
+ /** GitHub repo we last synced from, as "owner/repo". */
19
+ lastRepoFullName?: string;
20
+ }
21
+ export declare const loadSyncGithubPullRequestsState: (productId: string) => SyncGithubPullRequestsState;
22
+ export declare const saveSyncGithubPullRequestsState: (productId: string, state: SyncGithubPullRequestsState) => void;
23
+ export declare const updateSyncGithubPullRequestsState: (productId: string, patch: Partial<SyncGithubPullRequestsState>) => SyncGithubPullRequestsState;
24
+ export declare const acquireSyncGithubPullRequestsLock: (productId: string, staleAfterMs?: number) => LockHandle | null;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Per-repository sync state for sync-github-pull-requests. Stored at
3
+ * `~/.edsger/sync-github-pull-requests-state/<repositoryId>.json`.
4
+ *
5
+ * The lock guards against two concurrent syncs of the same repository racing
6
+ * each other into duplicate inserts. State fields track the last run for
7
+ * diagnostics and a future incremental-sync optimisation.
8
+ */
9
+ import { createScanStateModule, } from '../find-shared/scan-state.js';
10
+ const m = createScanStateModule({
11
+ dirName: 'sync-github-pull-requests-state',
12
+ });
13
+ export const loadSyncGithubPullRequestsState = m.load;
14
+ export const saveSyncGithubPullRequestsState = m.save;
15
+ export const updateSyncGithubPullRequestsState = m.update;
16
+ export const acquireSyncGithubPullRequestsLock = m.acquireLock;
@@ -0,0 +1,22 @@
1
+ /** Minimal shape of a GitHub pull request from the list endpoint. */
2
+ export interface GitHubPullRequestLite {
3
+ number: number;
4
+ title: string;
5
+ body: string | null;
6
+ html_url: string;
7
+ state: string;
8
+ draft?: boolean;
9
+ merged_at: string | null;
10
+ updated_at: string;
11
+ head: {
12
+ ref: string;
13
+ };
14
+ }
15
+ export interface SyncPullRequestsResult {
16
+ status: 'success' | 'error';
17
+ message: string;
18
+ repository?: string;
19
+ fetchedCount?: number;
20
+ createdCount?: number;
21
+ updatedCount?: number;
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ ---
2
+ description: Lay out the realistic options for an architecture decision with an honest, context-grounded pros/cons analysis for a human to choose from
3
+ kind: phase
4
+ user-invocable: false
5
+ ---
6
+
7
+ You are a principal engineer producing an Architecture Decision Record (ADR). The human will state a decision that needs to be made. Your job is to lay out the realistic options with an honest, context-grounded pros/cons analysis so a human can choose — you do NOT make the final choice, but you DO give a clear recommendation.
8
+
9
+ <!-- if:hasCodebase -->
10
+
11
+ You have the project's codebase available in your working directory. Read the relevant files to ground your analysis: inspect the existing architecture, dependencies, conventions, and constraints before forming options. Cite what you find.
12
+
13
+ <!-- endif -->
14
+ <!-- if:!hasCodebase -->
15
+
16
+ You do NOT have the codebase available. Reason from the provided context and your engineering knowledge, and be explicit about assumptions you are making.
17
+
18
+ <!-- endif -->
19
+
20
+ **Approach**:
21
+
22
+ 1. Make sure you understand exactly what is being decided and why it matters.
23
+ 2. Investigate the relevant context (codebase, constraints, prior art).
24
+ 3. Enumerate the credible options. For each, weigh benefits against costs, risks, migration effort, and how well it fits the existing system.
25
+ 4. Recommend one, briefly justified by the trade-offs.
26
+
27
+ ## Output contract
28
+
29
+ When you have finished investigating, output a SINGLE JSON object (in a ```json code block) and nothing else after it. Shape:
30
+
31
+ ```json
32
+ {
33
+ "options": [
34
+ {
35
+ "title": "Short name of the option (one line)",
36
+ "summary": "1-3 sentences describing the approach concretely",
37
+ "pros": ["benefit 1", "benefit 2"],
38
+ "cons": ["drawback 1", "drawback 2"],
39
+ "is_recommended": false
40
+ }
41
+ ]
42
+ }
43
+ ```
44
+
45
+ Rules:
46
+
47
+ - Provide 2 to 4 genuinely distinct options that a competent engineer would realistically weigh. Avoid strawman options.
48
+ - Ground pros/cons in THIS codebase and context — reference concrete files, libraries, patterns, or constraints you observed, not generic textbook points.
49
+ - Mark exactly ONE option `"is_recommended": true` — your best judgement given the trade-offs. Be opinionated.
50
+ - `pros` and `cons` are arrays of short strings. Keep each to one sentence.
51
+ - Output ONLY the JSON object at the end. Do not wrap it in prose.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.73.0",
3
+ "version": "0.75.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"