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,48 @@
1
+ /**
2
+ * Architecture Decision Record API for the CLI.
3
+ *
4
+ * The desktop app creates the `adrs` draft row (the human types the decision
5
+ * topic + context); the CLI agent reads it here, fills `adr_options` with
6
+ * pros/cons, and flips `generation_status` to `ready`. All writes go through the
7
+ * user-scoped Supabase client (RLS gates on product/repository access), so the
8
+ * CLI must run with a synced desktop session.
9
+ */
10
+ export interface AdrRow {
11
+ id: string;
12
+ product_id: string | null;
13
+ repository_id: string | null;
14
+ sequence: number;
15
+ title: string;
16
+ context: string | null;
17
+ generation_prompt: string | null;
18
+ }
19
+ export interface AdrOptionInput {
20
+ title: string;
21
+ summary?: string;
22
+ pros: string[];
23
+ cons: string[];
24
+ is_recommended?: boolean;
25
+ }
26
+ /** Context gathered for the agent prompt. */
27
+ export interface AdrContext {
28
+ scope: 'product' | 'repo';
29
+ name: string;
30
+ description: string | null;
31
+ /** Repo full names linked to the product, or the single repo's name. */
32
+ repos: string[];
33
+ /** Recent issue titles (product scope only) for situational awareness. */
34
+ issues: string[];
35
+ }
36
+ export declare function getAdr(adrId: string): Promise<AdrRow>;
37
+ /**
38
+ * Gather lightweight context for the decision: the product/repo identity, its
39
+ * linked repositories, and (for products) a sample of recent issue titles.
40
+ */
41
+ export declare function loadAdrContext(adr: AdrRow): Promise<AdrContext>;
42
+ /**
43
+ * Replace the ADR's options with a freshly generated set and mark the run
44
+ * ready for the human to choose. Clears any prior options so re-runs are clean.
45
+ */
46
+ export declare function saveAdrOptions(adrId: string, options: AdrOptionInput[]): Promise<void>;
47
+ export declare function markGenerationReady(adrId: string): Promise<void>;
48
+ export declare function markGenerationFailed(adrId: string, message: string): Promise<void>;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Architecture Decision Record API for the CLI.
3
+ *
4
+ * The desktop app creates the `adrs` draft row (the human types the decision
5
+ * topic + context); the CLI agent reads it here, fills `adr_options` with
6
+ * pros/cons, and flips `generation_status` to `ready`. All writes go through the
7
+ * user-scoped Supabase client (RLS gates on product/repository access), so the
8
+ * CLI must run with a synced desktop session.
9
+ */
10
+ import { ensureSupabaseSession, getSupabase } from '../supabase/client.js';
11
+ import { logInfo } from '../utils/logger.js';
12
+ async function requireSession() {
13
+ const ready = await ensureSupabaseSession();
14
+ if (!ready) {
15
+ throw new Error('Supabase session unavailable. Sign in to the Edsger desktop app to authorize the CLI.');
16
+ }
17
+ }
18
+ export async function getAdr(adrId) {
19
+ await requireSession();
20
+ const { data, error } = await getSupabase()
21
+ .from('adrs')
22
+ .select('id, product_id, repository_id, sequence, title, context, generation_prompt')
23
+ .eq('id', adrId)
24
+ .single();
25
+ if (error) {
26
+ throw new Error(`Failed to load ADR ${adrId}: ${error.message}`);
27
+ }
28
+ return data;
29
+ }
30
+ /**
31
+ * Gather lightweight context for the decision: the product/repo identity, its
32
+ * linked repositories, and (for products) a sample of recent issue titles.
33
+ */
34
+ export async function loadAdrContext(adr) {
35
+ const sb = getSupabase();
36
+ if (adr.product_id) {
37
+ const { data: product } = await sb
38
+ .from('products')
39
+ .select('name, description, product_repositories(repositories(full_name))')
40
+ .eq('id', adr.product_id)
41
+ .single();
42
+ const { data: issues } = await sb
43
+ .from('issues')
44
+ .select('name')
45
+ .eq('product_id', adr.product_id)
46
+ .order('created_at', { ascending: false })
47
+ .limit(25);
48
+ const repoLinks = (product?.product_repositories ??
49
+ []);
50
+ return {
51
+ scope: 'product',
52
+ name: product?.name ?? 'Unknown product',
53
+ description: product?.description ?? null,
54
+ repos: repoLinks
55
+ .flatMap((l) => Array.isArray(l.repositories)
56
+ ? l.repositories
57
+ : l.repositories
58
+ ? [l.repositories]
59
+ : [])
60
+ .map((r) => r.full_name)
61
+ .filter((n) => Boolean(n)),
62
+ issues: (issues ?? []).map((i) => i.name),
63
+ };
64
+ }
65
+ const { data: repo } = await sb
66
+ .from('repositories')
67
+ .select('full_name, description, language')
68
+ .eq('id', adr.repository_id)
69
+ .single();
70
+ return {
71
+ scope: 'repo',
72
+ name: repo?.full_name ?? 'Unknown repository',
73
+ description: repo?.description ?? null,
74
+ repos: repo?.full_name ? [repo.full_name] : [],
75
+ issues: [],
76
+ };
77
+ }
78
+ /**
79
+ * Replace the ADR's options with a freshly generated set and mark the run
80
+ * ready for the human to choose. Clears any prior options so re-runs are clean.
81
+ */
82
+ export async function saveAdrOptions(adrId, options) {
83
+ await requireSession();
84
+ const sb = getSupabase();
85
+ // Detach the previously selected option (FK) before clearing options, so the
86
+ // delete can't trip the ON DELETE SET NULL ordering on a re-run.
87
+ await sb.from('adrs').update({ selected_option_id: null }).eq('id', adrId);
88
+ const { error: delError } = await sb
89
+ .from('adr_options')
90
+ .delete()
91
+ .eq('adr_id', adrId);
92
+ if (delError) {
93
+ throw new Error(`Failed to clear prior options: ${delError.message}`);
94
+ }
95
+ if (options.length > 0) {
96
+ const rows = options.map((o, i) => ({
97
+ adr_id: adrId,
98
+ position: i,
99
+ title: o.title,
100
+ summary: o.summary ?? null,
101
+ pros: o.pros ?? [],
102
+ cons: o.cons ?? [],
103
+ is_recommended: o.is_recommended ?? false,
104
+ source: 'ai_generated',
105
+ }));
106
+ const { error } = await sb.from('adr_options').insert(rows);
107
+ if (error) {
108
+ throw new Error(`Failed to save options: ${error.message}`);
109
+ }
110
+ }
111
+ logInfo(`Saved ${options.length} decision options`);
112
+ }
113
+ export async function markGenerationReady(adrId) {
114
+ const { error } = await getSupabase()
115
+ .from('adrs')
116
+ .update({
117
+ generation_status: 'ready',
118
+ generation_error: null,
119
+ status: 'proposed',
120
+ })
121
+ .eq('id', adrId);
122
+ if (error) {
123
+ throw new Error(`Failed to mark ADR ready: ${error.message}`);
124
+ }
125
+ }
126
+ export async function markGenerationFailed(adrId, message) {
127
+ try {
128
+ await getSupabase()
129
+ .from('adrs')
130
+ .update({
131
+ generation_status: 'failed',
132
+ generation_error: message.slice(0, 1000),
133
+ })
134
+ .eq('id', adrId);
135
+ }
136
+ catch {
137
+ // Best-effort — the caller already logged the underlying error.
138
+ }
139
+ }
@@ -0,0 +1,13 @@
1
+ export interface AdrCommandOptions {
2
+ adrId: string;
3
+ verbose?: boolean;
4
+ }
5
+ /**
6
+ * Generate decision options for a draft ADR. The desktop app creates the draft
7
+ * row (the human types the decision topic) and passes its id here; this run
8
+ * fills it with options + pros/cons for the human to choose from.
9
+ *
10
+ * The cli_sessions `command` column is keyed off EDSGER_PROCESS_KEY (set by the
11
+ * desktop runner to `adr:<scopeId>`), so the originating tab can replay logs.
12
+ */
13
+ export declare const runAdr: (options: AdrCommandOptions) => Promise<void>;
@@ -0,0 +1,31 @@
1
+ import { generateAdr } from '../../phases/adr-generation/index.js';
2
+ import { deregisterSession, registerSession, } from '../../system/session-manager.js';
3
+ import { logError, logInfo } from '../../utils/logger.js';
4
+ import { validateConfiguration } from '../../utils/validation.js';
5
+ /**
6
+ * Generate decision options for a draft ADR. The desktop app creates the draft
7
+ * row (the human types the decision topic) and passes its id here; this run
8
+ * fills it with options + pros/cons for the human to choose from.
9
+ *
10
+ * The cli_sessions `command` column is keyed off EDSGER_PROCESS_KEY (set by the
11
+ * desktop runner to `adr:<scopeId>`), so the originating tab can replay logs.
12
+ */
13
+ export const runAdr = async (options) => {
14
+ const { adrId } = options;
15
+ // Establish CLI config (validates auth/env) before doing any work.
16
+ validateConfiguration({ verbose: options.verbose });
17
+ await registerSession({ command: 'adr' });
18
+ logInfo(`Starting ADR generation for: ${adrId}`);
19
+ try {
20
+ const result = await generateAdr({ adrId, verbose: options.verbose });
21
+ if (result.status !== 'success') {
22
+ logError(`ADR generation failed: ${result.error || 'Unknown error'}`);
23
+ }
24
+ }
25
+ catch (error) {
26
+ logError(`ADR generation error: ${error instanceof Error ? error.message : String(error)}`);
27
+ }
28
+ finally {
29
+ await deregisterSession();
30
+ }
31
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * CLI command: edsger features <productId> --scan-id <id>
3
+ *
4
+ * Clones every repository linked to the product, asks Claude to catalogue
5
+ * the user-facing features it delivers, and persists them via the
6
+ * product_features table. The desktop UI creates a pending feature_scans
7
+ * row first then invokes the CLI with --scan-id; the CLI flips status
8
+ * running → success/failed and writes features via the MCP toolkit.
9
+ */
10
+ export interface FeaturesCliOptions {
11
+ scanId: string;
12
+ guidance?: string;
13
+ verbose?: boolean;
14
+ }
15
+ export declare function runFeatures(productId: string, options: FeaturesCliOptions): Promise<void>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * CLI command: edsger features <productId> --scan-id <id>
3
+ *
4
+ * Clones every repository linked to the product, asks Claude to catalogue
5
+ * the user-facing features it delivers, and persists them via the
6
+ * product_features table. The desktop UI creates a pending feature_scans
7
+ * row first then invokes the CLI with --scan-id; the CLI flips status
8
+ * running → success/failed and writes features via the MCP toolkit.
9
+ */
10
+ import { runFeaturesPhase } from '../../phases/features/index.js';
11
+ import { logError, logInfo, logSuccess } from '../../utils/logger.js';
12
+ export async function runFeatures(productId, options) {
13
+ const { scanId, guidance, verbose } = options;
14
+ if (!productId) {
15
+ throw new Error('A product ID is required for features');
16
+ }
17
+ if (!scanId) {
18
+ throw new Error('--scan-id is required (the pending feature_scans row id)');
19
+ }
20
+ logInfo(`Starting features scan for product ${productId}`);
21
+ const result = await runFeaturesPhase({
22
+ productId,
23
+ scanId,
24
+ guidance,
25
+ verbose,
26
+ });
27
+ if (result.status === 'success') {
28
+ logSuccess(result.message);
29
+ }
30
+ else {
31
+ logError(result.message);
32
+ process.exit(1);
33
+ }
34
+ }
@@ -5,7 +5,9 @@
5
5
  export interface PRResolveCliOptions {
6
6
  prUrl: string;
7
7
  prId?: string;
8
+ /** Repo-only mode: resolve GitHub config from a repositories row. */
9
+ repoId?: string;
8
10
  verbose?: boolean;
9
11
  learn?: boolean;
10
12
  }
11
- export declare function runPRResolve(productId: string, options: PRResolveCliOptions): Promise<void>;
13
+ export declare function runPRResolve(productId: string | undefined, options: PRResolveCliOptions): Promise<void>;
@@ -2,31 +2,36 @@
2
2
  * CLI command: edsger pr-resolve <productId> --pr-url <url>
3
3
  * Resolves PR change requests: fixes what's genuinely better, explains what it won't change.
4
4
  */
5
- import { getGitHubConfigByProduct } from '../../api/github.js';
5
+ import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
6
6
  import { resolveStandalonePR } from '../../phases/pr-resolve/index.js';
7
7
  import { logError, logInfo, logSuccess } from '../../utils/logger.js';
8
8
  export async function runPRResolve(productId, options) {
9
- const { prUrl, prId, verbose } = options;
10
- logInfo(`Starting PR resolve for product ${productId}`);
9
+ const { prUrl, prId, repoId, verbose } = options;
10
+ const scopeLabel = repoId ? `repository ${repoId}` : `product ${productId}`;
11
+ logInfo(`Starting PR resolve for ${scopeLabel}`);
11
12
  logInfo(`PR URL: ${prUrl}`);
12
- // Get GitHub config via product developer settings
13
- const githubConfig = await getGitHubConfigByProduct(productId, verbose);
13
+ // Resolve GitHub config from the repository (repo-only mode) or the product.
14
+ const githubConfig = repoId
15
+ ? await getGitHubConfigByRepository(repoId, verbose)
16
+ : await getGitHubConfigByProduct(productId ?? '', verbose);
14
17
  if (!githubConfig.configured ||
15
18
  !githubConfig.token ||
16
19
  !githubConfig.owner ||
17
20
  !githubConfig.repo) {
18
- logError(`GitHub not configured for product ${productId}: ${githubConfig.message || 'No installation found'}`);
21
+ logError(`GitHub not configured for ${scopeLabel}: ${githubConfig.message || 'No installation found'}`);
19
22
  process.exit(1);
20
23
  }
21
24
  const result = await resolveStandalonePR({
22
25
  productId,
26
+ repositoryId: repoId,
23
27
  pullRequestUrl: prUrl,
24
28
  githubToken: githubConfig.token,
25
29
  owner: githubConfig.owner,
26
30
  repo: githubConfig.repo,
27
31
  prId,
28
32
  verbose,
29
- learn: options.learn,
33
+ // Checklist learning is product-scoped; skip it in repo-only mode.
34
+ learn: repoId ? false : options.learn,
30
35
  });
31
36
  if (result.status === 'success') {
32
37
  logSuccess(`PR resolve completed: ${result.message}`);
@@ -5,6 +5,8 @@
5
5
  export interface PRReviewCliOptions {
6
6
  prUrl: string;
7
7
  prId?: string;
8
+ /** Repo-only mode: resolve GitHub config from a repositories row. */
9
+ repoId?: string;
8
10
  verbose?: boolean;
9
11
  }
10
- export declare function runPRReview(productId: string, options: PRReviewCliOptions): Promise<void>;
12
+ export declare function runPRReview(productId: string | undefined, options: PRReviewCliOptions): Promise<void>;
@@ -2,24 +2,28 @@
2
2
  * CLI command: edsger pr-review <productId> --pr-url <url>
3
3
  * Reviews a standalone GitHub PR and posts comments.
4
4
  */
5
- import { getGitHubConfigByProduct } from '../../api/github.js';
5
+ import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
6
6
  import { reviewStandalonePR } from '../../phases/pr-review/index.js';
7
7
  import { logError, logInfo, logSuccess } from '../../utils/logger.js';
8
8
  export async function runPRReview(productId, options) {
9
- const { prUrl, prId, verbose } = options;
10
- logInfo(`Starting PR review for product ${productId}`);
9
+ const { prUrl, prId, repoId, verbose } = options;
10
+ const scopeLabel = repoId ? `repository ${repoId}` : `product ${productId}`;
11
+ logInfo(`Starting PR review for ${scopeLabel}`);
11
12
  logInfo(`PR URL: ${prUrl}`);
12
- // Get GitHub config via product developer settings
13
- const githubConfig = await getGitHubConfigByProduct(productId, verbose);
13
+ // Resolve GitHub config from the repository (repo-only mode) or the product.
14
+ const githubConfig = repoId
15
+ ? await getGitHubConfigByRepository(repoId, verbose)
16
+ : await getGitHubConfigByProduct(productId ?? '', verbose);
14
17
  if (!githubConfig.configured ||
15
18
  !githubConfig.token ||
16
19
  !githubConfig.owner ||
17
20
  !githubConfig.repo) {
18
- logError(`GitHub not configured for product ${productId}: ${githubConfig.message || 'No installation found'}`);
21
+ logError(`GitHub not configured for ${scopeLabel}: ${githubConfig.message || 'No installation found'}`);
19
22
  process.exit(1);
20
23
  }
21
24
  const result = await reviewStandalonePR({
22
25
  productId,
26
+ repositoryId: repoId,
23
27
  pullRequestUrl: prUrl,
24
28
  githubToken: githubConfig.token,
25
29
  owner: githubConfig.owner,
@@ -0,0 +1,11 @@
1
+ /**
2
+ * CLI command: `edsger sync-github-pull-requests <repoId>`
3
+ *
4
+ * Mirrors a connected GitHub repo's pull requests into the local
5
+ * `pull_requests` table (scoped by repository_id) for the repo detail page.
6
+ * Idempotent — already-synced PRs are refreshed, never duplicated.
7
+ */
8
+ export interface SyncGithubPullRequestsCliOptions {
9
+ verbose?: boolean;
10
+ }
11
+ export declare function runSyncGithubPullRequests(repositoryId: string, options?: SyncGithubPullRequestsCliOptions): Promise<void>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * CLI command: `edsger sync-github-pull-requests <repoId>`
3
+ *
4
+ * Mirrors a connected GitHub repo's pull requests into the local
5
+ * `pull_requests` table (scoped by repository_id) for the repo detail page.
6
+ * Idempotent — already-synced PRs are refreshed, never duplicated.
7
+ */
8
+ import { getGitHubConfigByRepository } from '../../api/github.js';
9
+ import { syncGithubPullRequests } from '../../phases/sync-github-pull-requests/index.js';
10
+ import { logError, logInfo, logSuccess } from '../../utils/logger.js';
11
+ export async function runSyncGithubPullRequests(repositoryId, options = {}) {
12
+ const { verbose } = options;
13
+ logInfo(`Starting GitHub pull request sync for repository ${repositoryId}`);
14
+ const githubConfig = await getGitHubConfigByRepository(repositoryId, verbose);
15
+ if (!githubConfig.configured ||
16
+ !githubConfig.token ||
17
+ !githubConfig.owner ||
18
+ !githubConfig.repo) {
19
+ logError(`GitHub not configured for repository ${repositoryId}: ${githubConfig.message || 'No installation found'}`);
20
+ process.exit(1);
21
+ }
22
+ const result = await syncGithubPullRequests({
23
+ repositoryId,
24
+ githubToken: githubConfig.token,
25
+ owner: githubConfig.owner,
26
+ repo: githubConfig.repo,
27
+ verbose,
28
+ });
29
+ if (result.status === 'success') {
30
+ logSuccess(`GitHub PR sync completed: ${result.message}`);
31
+ if (result.repository) {
32
+ logInfo(`Repository: ${result.repository}`);
33
+ }
34
+ if (result.fetchedCount !== undefined) {
35
+ logInfo(`Fetched ${result.fetchedCount} · created ${result.createdCount ?? 0} · updated ${result.updatedCount ?? 0}`);
36
+ }
37
+ }
38
+ else {
39
+ logError(`GitHub PR sync failed: ${result.message}`);
40
+ process.exit(1);
41
+ }
42
+ }
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { dirname, join } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { applyEnvStore } from './auth/env-store.js';
8
8
  import { runLogin, runLogout, runStatus } from './auth/login.js';
9
+ import { runAdr } from './commands/adr/index.js';
9
10
  import { runAgentWorkflow } from './commands/agent-workflow/index.js';
10
11
  import { runAnalyzeLogs } from './commands/analyze-logs/index.js';
11
12
  import { runAppStoreGeneration } from './commands/app-store/index.js';
@@ -19,6 +20,7 @@ import { runConfigGet, runConfigList, runConfigSet, runConfigUnset, } from './co
19
20
  import { runDataFlow } from './commands/data-flow/index.js';
20
21
  import { runDiscover } from './commands/discover/index.js';
21
22
  import { runErDiagram } from './commands/er-diagram/index.js';
23
+ import { runFeatures } from './commands/features/index.js';
22
24
  import { runFinancingDeck } from './commands/financing-deck/index.js';
23
25
  import { runFindArchitecture } from './commands/find-architecture/index.js';
24
26
  import { runFindBugs } from './commands/find-bugs/index.js';
@@ -46,6 +48,7 @@ import { runStateDiagram } from './commands/state-diagram/index.js';
46
48
  import { runSyncAws } from './commands/sync-aws/index.js';
47
49
  import { runSyncDatadog } from './commands/sync-datadog/index.js';
48
50
  import { runSyncGithubIssues } from './commands/sync-github-issues/index.js';
51
+ import { runSyncGithubPullRequests } from './commands/sync-github-pull-requests/index.js';
49
52
  import { runSyncOrgRepos } from './commands/sync-org-repos/index.js';
50
53
  import { runSyncSentryIssues } from './commands/sync-sentry-issues/index.js';
51
54
  import { runSyncTerraform } from './commands/sync-terraform/index.js';
@@ -462,6 +465,21 @@ program
462
465
  }
463
466
  });
464
467
  // ============================================================
468
+ // Subcommand: edsger adr <adrId>
469
+ // ============================================================
470
+ program
471
+ .command('adr <adrId>')
472
+ .description('Generate decision options (pros/cons) for a draft Architecture Decision Record')
473
+ .option('-v, --verbose', 'Verbose output')
474
+ .action(async (adrId, opts) => {
475
+ try {
476
+ await runAdr({ adrId, verbose: opts.verbose });
477
+ }
478
+ catch (error) {
479
+ exitWithError(error);
480
+ }
481
+ });
482
+ // ============================================================
465
483
  // Subcommand: edsger app-store <productId>
466
484
  // ============================================================
467
485
  program
@@ -763,13 +781,17 @@ program
763
781
  // Subcommand: edsger pr-review <productId>
764
782
  // ============================================================
765
783
  program
766
- .command('pr-review <productId>')
767
- .description('AI-review a GitHub PR for a product')
784
+ .command('pr-review [productId]')
785
+ .description('AI-review a GitHub PR for a product (or standalone repository)')
768
786
  .requiredOption('--pr-url <url>', 'GitHub PR URL')
769
787
  .option('--pr-id <id>', 'Pull request record ID in database')
788
+ .option('--repo-id <id>', 'Run in repo-only mode against a single repositories row (no product context)')
770
789
  .option('-v, --verbose', 'Verbose output')
771
790
  .action(async (productId, opts) => {
772
791
  try {
792
+ if (!productId && !opts.repoId) {
793
+ throw new Error('Provide a productId or --repo-id (repo-only mode) for pr-review');
794
+ }
773
795
  await runPRReview(productId, opts);
774
796
  }
775
797
  catch (error) {
@@ -842,6 +864,21 @@ program
842
864
  }
843
865
  });
844
866
  // ============================================================
867
+ // Subcommand: edsger sync-github-pull-requests <repoId>
868
+ // ============================================================
869
+ program
870
+ .command('sync-github-pull-requests <repoId>')
871
+ .description("Mirror a connected GitHub repo's pull requests into the local pull_requests list for the repo detail page (idempotent — refreshes, never duplicates)")
872
+ .option('-v, --verbose', 'Verbose output')
873
+ .action(async (repoId, opts) => {
874
+ try {
875
+ await runSyncGithubPullRequests(repoId, opts);
876
+ }
877
+ catch (error) {
878
+ exitWithError(error);
879
+ }
880
+ });
881
+ // ============================================================
845
882
  // Subcommand: edsger sync-org-repos <teamId>
846
883
  // ============================================================
847
884
  program
@@ -1081,17 +1118,42 @@ program
1081
1118
  }
1082
1119
  });
1083
1120
  // ============================================================
1121
+ // Subcommand: edsger features <productId>
1122
+ // ============================================================
1123
+ program
1124
+ .command('features <productId>')
1125
+ .description('Scan every repository linked to a product for the user-facing features it delivers and persist them via the product_features table. Manually defined features are enriched, never rewritten. Writes against the pending feature_scans row identified by --scan-id.')
1126
+ .requiredOption('--scan-id <id>', 'Pending feature_scans row id to drive (created by the desktop UI before invocation)')
1127
+ .option('-g, --guidance <text>', 'Human direction for the AI (focus areas, exclusions)')
1128
+ .option('-v, --verbose', 'Verbose output')
1129
+ .action(async (productId, opts) => {
1130
+ try {
1131
+ await runFeatures(productId, {
1132
+ scanId: opts.scanId,
1133
+ guidance: opts.guidance,
1134
+ verbose: opts.verbose,
1135
+ });
1136
+ }
1137
+ catch (error) {
1138
+ exitWithError(error);
1139
+ }
1140
+ });
1141
+ // ============================================================
1084
1142
  // Subcommand: edsger pr-resolve <productId>
1085
1143
  // ============================================================
1086
1144
  program
1087
- .command('pr-resolve <productId>')
1088
- .description('AI-resolve change requests on a GitHub PR')
1145
+ .command('pr-resolve [productId]')
1146
+ .description('AI-resolve change requests on a GitHub PR for a product (or standalone repository)')
1089
1147
  .requiredOption('--pr-url <url>', 'GitHub PR URL')
1090
1148
  .option('--pr-id <id>', 'Pull request record ID in database')
1149
+ .option('--repo-id <id>', 'Run in repo-only mode against a single repositories row (no product context)')
1091
1150
  .option('--no-learn', 'Skip checklist learning after resolve')
1092
1151
  .option('-v, --verbose', 'Verbose output')
1093
1152
  .action(async (productId, opts) => {
1094
1153
  try {
1154
+ if (!productId && !opts.repoId) {
1155
+ throw new Error('Provide a productId or --repo-id (repo-only mode) for pr-resolve');
1156
+ }
1095
1157
  await runPRResolve(productId, opts);
1096
1158
  }
1097
1159
  catch (error) {
@@ -0,0 +1,6 @@
1
+ import { type AdrOptionInput } from '../../api/adr.js';
2
+ /**
3
+ * Run the ADR agent. Returns the validated options, or null if the model
4
+ * produced nothing parseable.
5
+ */
6
+ export declare function executeAdrQuery(userPrompt: string, systemPrompt: string, verbose?: boolean, cwd?: string): Promise<AdrOptionInput[] | null>;
@@ -0,0 +1,69 @@
1
+ import { query } from '@anthropic-ai/claude-agent-sdk';
2
+ import { DEFAULT_MODEL } from '../../constants.js';
3
+ import { logDebug, logError, logInfo } from '../../utils/logger.js';
4
+ import { extractJSON, normalizeOptions } from './parse.js';
5
+ // eslint-disable-next-line @typescript-eslint/require-await
6
+ async function* promptStream(text) {
7
+ yield { type: 'user', message: { role: 'user', content: text } };
8
+ }
9
+ /**
10
+ * Run the ADR agent. Returns the validated options, or null if the model
11
+ * produced nothing parseable.
12
+ */
13
+ export async function executeAdrQuery(userPrompt, systemPrompt, verbose, cwd) {
14
+ let lastAssistant = '';
15
+ let result = null;
16
+ let turnCount = 0;
17
+ logInfo('Connecting to AI agent for decision analysis...');
18
+ for await (const message of query({
19
+ prompt: promptStream(userPrompt),
20
+ options: {
21
+ systemPrompt: {
22
+ type: 'preset',
23
+ preset: 'claude_code',
24
+ append: systemPrompt,
25
+ },
26
+ model: DEFAULT_MODEL,
27
+ maxTurns: 1000,
28
+ permissionMode: 'bypassPermissions',
29
+ ...(cwd ? { cwd } : {}),
30
+ },
31
+ })) {
32
+ if (message.type === 'assistant' && message.message?.content) {
33
+ turnCount++;
34
+ for (const content of message.message.content) {
35
+ if (content.type === 'text') {
36
+ lastAssistant += `${content.text}\n`;
37
+ logDebug(`${content.text}`, verbose);
38
+ }
39
+ else if (content.type === 'tool_use') {
40
+ const input = (content.input ?? {});
41
+ const desc = input.description ?? input.command ?? 'Running...';
42
+ logInfo(`[Turn ${turnCount}] ${content.name}: ${typeof desc === 'string' ? desc.slice(0, 120) : 'Running...'}`);
43
+ }
44
+ }
45
+ }
46
+ if (message.type === 'result') {
47
+ const text = message.subtype === 'success'
48
+ ? message.result || lastAssistant
49
+ : lastAssistant;
50
+ if (message.subtype === 'success') {
51
+ logInfo(`\nAnalysis completed after ${turnCount} turns, parsing...`);
52
+ }
53
+ else {
54
+ logError(`\nAnalysis incomplete: ${message.subtype}`);
55
+ }
56
+ const parsed = extractJSON(text);
57
+ if (parsed) {
58
+ const options = normalizeOptions(parsed);
59
+ if (options.length > 0) {
60
+ result = options;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ if (!result) {
66
+ logError('Could not extract decision options from the agent response.');
67
+ }
68
+ return result;
69
+ }