edsger 0.76.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/api-docs/index.d.ts +18 -0
- package/dist/commands/api-docs/index.js +41 -0
- package/dist/commands/quality-benchmark/index.d.ts +5 -0
- package/dist/commands/quality-benchmark/index.js +28 -0
- package/dist/index.js +29 -0
- package/dist/phases/api-docs/index.d.ts +47 -0
- package/dist/phases/api-docs/index.js +254 -0
- package/dist/phases/api-docs/mcp-server.d.ts +25 -0
- package/dist/phases/api-docs/mcp-server.js +82 -0
- package/dist/phases/api-docs/prompts.d.ts +16 -0
- package/dist/phases/api-docs/prompts.js +65 -0
- package/dist/phases/api-docs/types.d.ts +22 -0
- package/dist/phases/api-docs/types.js +10 -0
- package/dist/phases/find-architecture/index.js +13 -6
- package/dist/phases/find-architecture/prompts.d.ts +2 -1
- package/dist/phases/find-architecture/prompts.js +3 -2
- package/dist/phases/find-bugs/index.js +3 -1
- package/dist/phases/find-shared/baseline.d.ts +45 -0
- package/dist/phases/find-shared/baseline.js +56 -0
- package/dist/phases/find-shared/custom-rules.d.ts +39 -0
- package/dist/phases/find-shared/custom-rules.js +75 -0
- package/dist/phases/find-shared/detect-context.d.ts +40 -0
- package/dist/phases/find-shared/detect-context.js +247 -0
- package/dist/phases/find-shared/mcp.d.ts +6 -0
- package/dist/phases/find-shared/mcp.js +2 -0
- package/dist/phases/find-shared/rule-config.d.ts +37 -0
- package/dist/phases/find-shared/rule-config.js +67 -0
- package/dist/phases/find-shared/rule-packs.d.ts +65 -0
- package/dist/phases/find-shared/rule-packs.js +124 -0
- package/dist/phases/find-shared/scoped-read.d.ts +12 -0
- package/dist/phases/find-shared/scoped-read.js +33 -0
- package/dist/phases/find-smells/index.js +12 -5
- package/dist/phases/find-smells/prompts.d.ts +2 -1
- package/dist/phases/find-smells/prompts.js +4 -3
- package/dist/phases/quality-benchmark/gate.d.ts +50 -0
- package/dist/phases/quality-benchmark/gate.js +91 -0
- package/dist/phases/quality-benchmark/index.js +15 -1
- package/dist/phases/quality-benchmark/parsers.d.ts +23 -0
- package/dist/phases/quality-benchmark/parsers.js +210 -0
- package/dist/phases/quality-benchmark/rubric.md +37 -0
- package/dist/phases/quality-benchmark/tool-catalog.js +58 -1
- package/dist/phases/quality-benchmark/types.d.ts +8 -1
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: edsger api-docs <productId> --run-id <id>
|
|
3
|
+
* edsger api-docs --repo-id <id> --run-id <id>
|
|
4
|
+
*
|
|
5
|
+
* Clones the repository, asks Claude to determine its API surface, and persists
|
|
6
|
+
* a generated API reference (Markdown + optional OpenAPI document) onto the
|
|
7
|
+
* pending api_doc_runs row. The desktop UI creates the pending row first then
|
|
8
|
+
* invokes the CLI with --run-id; the CLI flips status running →
|
|
9
|
+
* success/failed and writes the artifact via the api-docs MCP toolkit.
|
|
10
|
+
*/
|
|
11
|
+
export interface ApiDocsCliOptions {
|
|
12
|
+
runId: string;
|
|
13
|
+
/** Repo-only mode: generate for a single repositories row, no product. */
|
|
14
|
+
repoId?: string;
|
|
15
|
+
guidance?: string;
|
|
16
|
+
verbose?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function runApiDocs(productId: string | undefined, options: ApiDocsCliOptions): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: edsger api-docs <productId> --run-id <id>
|
|
3
|
+
* edsger api-docs --repo-id <id> --run-id <id>
|
|
4
|
+
*
|
|
5
|
+
* Clones the repository, asks Claude to determine its API surface, and persists
|
|
6
|
+
* a generated API reference (Markdown + optional OpenAPI document) onto the
|
|
7
|
+
* pending api_doc_runs row. The desktop UI creates the pending row first then
|
|
8
|
+
* invokes the CLI with --run-id; the CLI flips status running →
|
|
9
|
+
* success/failed and writes the artifact via the api-docs MCP toolkit.
|
|
10
|
+
*/
|
|
11
|
+
import { runApiDocsPhase } from '../../phases/api-docs/index.js';
|
|
12
|
+
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
13
|
+
export async function runApiDocs(productId, options) {
|
|
14
|
+
const { runId, repoId, guidance, verbose } = options;
|
|
15
|
+
if (!productId && !repoId) {
|
|
16
|
+
throw new Error('Either a product ID or --repo-id is required for api-docs');
|
|
17
|
+
}
|
|
18
|
+
if (!runId) {
|
|
19
|
+
throw new Error('--run-id is required (the pending api_doc_runs row id)');
|
|
20
|
+
}
|
|
21
|
+
if (productId) {
|
|
22
|
+
logInfo(`Starting API docs generation for product ${productId}`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
logInfo(`Starting API docs generation for repository ${repoId}`);
|
|
26
|
+
}
|
|
27
|
+
const result = await runApiDocsPhase({
|
|
28
|
+
productId,
|
|
29
|
+
repoId,
|
|
30
|
+
runId,
|
|
31
|
+
guidance,
|
|
32
|
+
verbose,
|
|
33
|
+
});
|
|
34
|
+
if (result.status === 'success') {
|
|
35
|
+
logSuccess(result.message);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
logError(result.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -36,5 +36,10 @@ export interface QualityBenchmarkCliOptions {
|
|
|
36
36
|
save?: boolean;
|
|
37
37
|
/** Overwrite any existing report row for this (product, commit, rubric). */
|
|
38
38
|
force?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Evaluate the scope's quality gate against the report and exit non-zero if
|
|
41
|
+
* it fails — for use in CI ("fail the build"). No-op when no gate is set.
|
|
42
|
+
*/
|
|
43
|
+
gate?: boolean;
|
|
39
44
|
}
|
|
40
45
|
export declare function runQualityBenchmarkCli(productId: string, options: QualityBenchmarkCliOptions): Promise<void>;
|
|
@@ -26,6 +26,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
26
26
|
import { dirname, resolve } from 'node:path';
|
|
27
27
|
import { getGitHubConfigByProduct, getGitHubConfigByRepository, getRepositoryBasics, } from '../../api/github.js';
|
|
28
28
|
import { fetchProductBasics } from '../../phases/find-shared/mcp.js';
|
|
29
|
+
import { evaluateGate, getQualityGate, } from '../../phases/quality-benchmark/gate.js';
|
|
29
30
|
import { runQualityBenchmark, } from '../../phases/quality-benchmark/index.js';
|
|
30
31
|
import { prepareQualityWorkspace } from '../../phases/quality-benchmark/workspace.js';
|
|
31
32
|
import { saveQualityReport } from '../../services/quality-reports.js';
|
|
@@ -207,6 +208,33 @@ export async function runQualityBenchmarkCli(productId, options) {
|
|
|
207
208
|
if (outcome.report.executive_summary) {
|
|
208
209
|
logInfo(outcome.report.executive_summary);
|
|
209
210
|
}
|
|
211
|
+
// Quality gate (CI enforcement). Evaluated against the report we just
|
|
212
|
+
// produced; a failing gate exits non-zero so a pipeline step can block.
|
|
213
|
+
if (options.gate) {
|
|
214
|
+
const scope = repoOnly
|
|
215
|
+
? { repoId: options.repoId }
|
|
216
|
+
: { productId };
|
|
217
|
+
const gate = await getQualityGate(scope);
|
|
218
|
+
if (!gate) {
|
|
219
|
+
logInfo('Quality gate: none configured for this scope — skipping.');
|
|
220
|
+
}
|
|
221
|
+
else if (!gate.enabled) {
|
|
222
|
+
logInfo('Quality gate: disabled for this scope — skipping.');
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const result = evaluateGate(outcome.report, gate);
|
|
226
|
+
if (result.passed) {
|
|
227
|
+
logSuccess('Quality gate: PASS');
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
logError('Quality gate: FAIL');
|
|
231
|
+
for (const v of result.violations) {
|
|
232
|
+
logError(` • ${v.label}: required ${v.required}, got ${v.actual}`);
|
|
233
|
+
}
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
210
238
|
}
|
|
211
239
|
/** Resolve a repository id to its "owner/repo" full name (RLS-scoped). */
|
|
212
240
|
async function resolveRepositoryFullName(repositoryId) {
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { runLogin, runLogout, runStatus } from './auth/login.js';
|
|
|
9
9
|
import { runAdr } from './commands/adr/index.js';
|
|
10
10
|
import { runAgentWorkflow } from './commands/agent-workflow/index.js';
|
|
11
11
|
import { runAnalyzeLogs } from './commands/analyze-logs/index.js';
|
|
12
|
+
import { runApiDocs } from './commands/api-docs/index.js';
|
|
12
13
|
import { runAppStoreGeneration } from './commands/app-store/index.js';
|
|
13
14
|
import { runArchitectureDiagram } from './commands/architecture-diagram/index.js';
|
|
14
15
|
import { runBuild } from './commands/build/index.js';
|
|
@@ -837,6 +838,7 @@ program
|
|
|
837
838
|
.option('--output <path>', 'Where to write the JSON report (default: ./quality-report-<commit>.json)')
|
|
838
839
|
.option('--no-save', 'Skip the quality_reports/save MCP call (local-only run)')
|
|
839
840
|
.option('--force', 'Overwrite any existing report row for this (product, commit, rubric)')
|
|
841
|
+
.option('--gate', "Evaluate the scope's quality gate against the report and exit non-zero if it fails (for CI). No-op when no gate is configured")
|
|
840
842
|
.option('-v, --verbose', 'Print every progress event')
|
|
841
843
|
.action(async (productId, opts) => {
|
|
842
844
|
try {
|
|
@@ -1142,6 +1144,33 @@ program
|
|
|
1142
1144
|
}
|
|
1143
1145
|
});
|
|
1144
1146
|
// ============================================================
|
|
1147
|
+
// Subcommand: edsger api-docs <productId>
|
|
1148
|
+
// ============================================================
|
|
1149
|
+
program
|
|
1150
|
+
.command('api-docs [productId]')
|
|
1151
|
+
.description("Generate an API reference (Markdown + an optional OpenAPI 3.x document) for a product's (or standalone repository's) codebase by determining the API surface it exposes from its source. Writes against the pending api_doc_runs row identified by --run-id.")
|
|
1152
|
+
.requiredOption('--run-id <id>', 'Pending api_doc_runs row id to drive (created by the desktop UI before invocation)')
|
|
1153
|
+
.option('--repo-id <id>', 'Run in repo-only mode against a single repositories row (no product context)')
|
|
1154
|
+
.option('-g, --guidance <text>', 'Human direction for the AI (focus areas, exclusions)')
|
|
1155
|
+
.option('-v, --verbose', 'Verbose output')
|
|
1156
|
+
.action(async (productId, opts) => {
|
|
1157
|
+
try {
|
|
1158
|
+
if (!productId && !opts.repoId) {
|
|
1159
|
+
throw new Error('Provide a productId or --repo-id (repo-only mode) for api-docs');
|
|
1160
|
+
}
|
|
1161
|
+
await runApiDocs(productId, {
|
|
1162
|
+
runId: opts.runId,
|
|
1163
|
+
repoId: opts.repoId,
|
|
1164
|
+
guidance: opts.guidance,
|
|
1165
|
+
verbose: opts.verbose,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
1170
|
+
process.exit(1);
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
// ============================================================
|
|
1145
1174
|
// Subcommand: edsger pr-resolve <productId>
|
|
1146
1175
|
// ============================================================
|
|
1147
1176
|
program
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API docs phase: clone the repository, ask Claude to work out the API surface
|
|
3
|
+
* it exposes, and persist a generated API reference (Markdown + an optional
|
|
4
|
+
* OpenAPI document) onto the pending api_doc_runs row.
|
|
5
|
+
*
|
|
6
|
+
* Production-grade behaviours (mirrors the recipes phase):
|
|
7
|
+
*
|
|
8
|
+
* - Heartbeat: `last_heartbeat_at` on the api_doc_runs row is refreshed on
|
|
9
|
+
* every assistant message so the reader can detect stalled / crashed runs
|
|
10
|
+
* (see desktop-app/.../services/db/api-doc-runs.ts for the lazy reaper).
|
|
11
|
+
* - Cancellation-safe writes: markRunning / persistAndSucceed / markFailed
|
|
12
|
+
* only touch rows whose status is in {pending, running}. If the user
|
|
13
|
+
* clicked Stop and the row is now 'cancelled', the final write no-ops.
|
|
14
|
+
* - Capture-then-persist: the agent submits the reference via a single
|
|
15
|
+
* `submit_api_docs` MCP tool; the artifact is written in one atomic update
|
|
16
|
+
* alongside the success flip, so a run never leaves a half-written doc.
|
|
17
|
+
*/
|
|
18
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
19
|
+
import type { ApiDocsArtifact } from './types.js';
|
|
20
|
+
export interface ApiDocsPhaseOptions {
|
|
21
|
+
/** Product-scoped run. Mutually exclusive with `repoId`. */
|
|
22
|
+
productId?: string;
|
|
23
|
+
/** Repo-only run: a single repositories row, no product context. */
|
|
24
|
+
repoId?: string;
|
|
25
|
+
runId: string;
|
|
26
|
+
guidance?: string;
|
|
27
|
+
verbose?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ApiDocsPhaseResult {
|
|
30
|
+
status: 'success' | 'error' | 'cancelled';
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function runApiDocsPhase(options: ApiDocsPhaseOptions): Promise<ApiDocsPhaseResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Claim the row by flipping `pending` → `running`. Returns true on success and
|
|
36
|
+
* false when the row has already moved on (e.g. user cancelled before the CLI
|
|
37
|
+
* started). Bounded by the status filter so we can't resurrect a 'cancelled'
|
|
38
|
+
* row.
|
|
39
|
+
*/
|
|
40
|
+
export declare function markRunning(supabase: SupabaseClient, runId: string): Promise<boolean>;
|
|
41
|
+
export declare function heartbeat(supabase: SupabaseClient, runId: string): Promise<void>;
|
|
42
|
+
export declare function markFailed(supabase: SupabaseClient, runId: string, errorMessage: string): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Write the captured artifact AND flip the row to success in one update,
|
|
45
|
+
* bounded by the {pending, running} status filter so a cancelled run no-ops.
|
|
46
|
+
*/
|
|
47
|
+
export declare function persistAndSucceed(supabase: SupabaseClient, runId: string, artifact: ApiDocsArtifact): Promise<boolean>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API docs phase: clone the repository, ask Claude to work out the API surface
|
|
3
|
+
* it exposes, and persist a generated API reference (Markdown + an optional
|
|
4
|
+
* OpenAPI document) onto the pending api_doc_runs row.
|
|
5
|
+
*
|
|
6
|
+
* Production-grade behaviours (mirrors the recipes phase):
|
|
7
|
+
*
|
|
8
|
+
* - Heartbeat: `last_heartbeat_at` on the api_doc_runs row is refreshed on
|
|
9
|
+
* every assistant message so the reader can detect stalled / crashed runs
|
|
10
|
+
* (see desktop-app/.../services/db/api-doc-runs.ts for the lazy reaper).
|
|
11
|
+
* - Cancellation-safe writes: markRunning / persistAndSucceed / markFailed
|
|
12
|
+
* only touch rows whose status is in {pending, running}. If the user
|
|
13
|
+
* clicked Stop and the row is now 'cancelled', the final write no-ops.
|
|
14
|
+
* - Capture-then-persist: the agent submits the reference via a single
|
|
15
|
+
* `submit_api_docs` MCP tool; the artifact is written in one atomic update
|
|
16
|
+
* alongside the success flip, so a run never leaves a half-written doc.
|
|
17
|
+
*/
|
|
18
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
19
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, getRepositoryBasics, } from '../../api/github.js';
|
|
20
|
+
import { DEFAULT_MODEL } from '../../constants.js';
|
|
21
|
+
import { getSupabase } from '../../supabase/client.js';
|
|
22
|
+
import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
|
|
23
|
+
import { cleanupIssueRepo, cloneIssueRepo, ensureWorkspaceDir, } from '../../workspace/workspace-manager.js';
|
|
24
|
+
import { fetchProductBasics } from '../find-shared/mcp.js';
|
|
25
|
+
import { createPromptGenerator, extractTextFromContent, } from '../pr-shared/agent-utils.js';
|
|
26
|
+
import { createApiDocsCaptureState, createApiDocsMcpServer, } from './mcp-server.js';
|
|
27
|
+
import { createApiDocsSystemPrompt, createApiDocsUserPrompt, } from './prompts.js';
|
|
28
|
+
const WORKSPACE_KEY = 'api-docs';
|
|
29
|
+
const MAX_TURNS = 200;
|
|
30
|
+
// Heartbeat cadence: at most one DB write per HEARTBEAT_MIN_INTERVAL_MS.
|
|
31
|
+
const HEARTBEAT_MIN_INTERVAL_MS = 15_000;
|
|
32
|
+
export async function runApiDocsPhase(options) {
|
|
33
|
+
const { productId, repoId, runId, guidance, verbose } = options;
|
|
34
|
+
const repoOnly = !productId && Boolean(repoId);
|
|
35
|
+
if (productId) {
|
|
36
|
+
logInfo(`Starting API docs generation for product ${productId}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
logInfo(`Starting API docs generation for repository ${repoId}`);
|
|
40
|
+
}
|
|
41
|
+
const supabase = getSupabase();
|
|
42
|
+
const claimed = await markRunning(supabase, runId);
|
|
43
|
+
if (!claimed) {
|
|
44
|
+
return {
|
|
45
|
+
status: 'cancelled',
|
|
46
|
+
message: 'API docs run row is no longer in a runnable state (likely cancelled before the CLI started)',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Resolve repo basics (name/description) for the prompt.
|
|
50
|
+
let repoBasics = {
|
|
51
|
+
fullName: null,
|
|
52
|
+
description: null,
|
|
53
|
+
};
|
|
54
|
+
if (repoOnly) {
|
|
55
|
+
const basics = await getRepositoryBasics(repoId);
|
|
56
|
+
repoBasics = { fullName: basics.fullName, description: basics.description };
|
|
57
|
+
}
|
|
58
|
+
const githubConfig = repoOnly
|
|
59
|
+
? await getGitHubConfigByRepository(repoId, verbose)
|
|
60
|
+
: await getGitHubConfigByProduct(productId, verbose);
|
|
61
|
+
if (!githubConfig.configured ||
|
|
62
|
+
!githubConfig.token ||
|
|
63
|
+
!githubConfig.owner ||
|
|
64
|
+
!githubConfig.repo) {
|
|
65
|
+
const msg = githubConfig.message ||
|
|
66
|
+
(repoOnly
|
|
67
|
+
? 'GitHub repository not configured. Connect the repo first.'
|
|
68
|
+
: 'GitHub repository not configured for this product. Connect a repo first.');
|
|
69
|
+
await markFailed(supabase, runId, msg);
|
|
70
|
+
return { status: 'error', message: msg };
|
|
71
|
+
}
|
|
72
|
+
let repoPath;
|
|
73
|
+
let succeeded = false;
|
|
74
|
+
try {
|
|
75
|
+
const workspaceRoot = ensureWorkspaceDir();
|
|
76
|
+
const repoKey = repoOnly
|
|
77
|
+
? `${WORKSPACE_KEY}-repo-${repoId}`
|
|
78
|
+
: `${WORKSPACE_KEY}-${productId}`;
|
|
79
|
+
({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, githubConfig.owner, githubConfig.repo, githubConfig.token));
|
|
80
|
+
const basics = repoOnly
|
|
81
|
+
? {
|
|
82
|
+
name: repoBasics.fullName ?? `${githubConfig.owner}/${githubConfig.repo}`,
|
|
83
|
+
description: repoBasics.description ?? undefined,
|
|
84
|
+
}
|
|
85
|
+
: await fetchProductBasics(productId);
|
|
86
|
+
const systemPrompt = createApiDocsSystemPrompt();
|
|
87
|
+
const userPrompt = createApiDocsUserPrompt({
|
|
88
|
+
repoName: basics.name,
|
|
89
|
+
repoDescription: basics.description,
|
|
90
|
+
guidance,
|
|
91
|
+
});
|
|
92
|
+
const capture = createApiDocsCaptureState();
|
|
93
|
+
const mcpServer = createApiDocsMcpServer(capture);
|
|
94
|
+
logInfo('Running Claude agent to generate API docs...');
|
|
95
|
+
let lastHeartbeatAt = 0;
|
|
96
|
+
for await (const message of query({
|
|
97
|
+
prompt: createPromptGenerator(userPrompt),
|
|
98
|
+
options: {
|
|
99
|
+
systemPrompt: {
|
|
100
|
+
type: 'preset',
|
|
101
|
+
preset: 'claude_code',
|
|
102
|
+
append: systemPrompt,
|
|
103
|
+
},
|
|
104
|
+
model: DEFAULT_MODEL,
|
|
105
|
+
maxTurns: MAX_TURNS,
|
|
106
|
+
permissionMode: 'bypassPermissions',
|
|
107
|
+
cwd: repoPath,
|
|
108
|
+
mcpServers: {
|
|
109
|
+
'api-docs': mcpServer,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
})) {
|
|
113
|
+
if (message.type === 'assistant') {
|
|
114
|
+
extractTextFromContent(message.message?.content ?? [], verbose);
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
if (now - lastHeartbeatAt >= HEARTBEAT_MIN_INTERVAL_MS) {
|
|
117
|
+
lastHeartbeatAt = now;
|
|
118
|
+
await heartbeat(supabase, runId);
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (message.type !== 'result') {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (message.subtype !== 'success') {
|
|
126
|
+
const msg = `API docs generation failed: agent ${message.subtype}`;
|
|
127
|
+
const written = await markFailed(supabase, runId, msg);
|
|
128
|
+
return {
|
|
129
|
+
status: written ? 'error' : 'cancelled',
|
|
130
|
+
message: written
|
|
131
|
+
? msg
|
|
132
|
+
: 'Run was cancelled while the agent was running',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (!capture.captured) {
|
|
136
|
+
const msg = 'Agent finished without calling submit_api_docs — no reference was produced.';
|
|
137
|
+
const written = await markFailed(supabase, runId, msg);
|
|
138
|
+
return {
|
|
139
|
+
status: written ? 'error' : 'cancelled',
|
|
140
|
+
message: written ? msg : 'Run was cancelled before completion',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const written = await persistAndSucceed(supabase, runId, capture.captured);
|
|
144
|
+
if (!written) {
|
|
145
|
+
return {
|
|
146
|
+
status: 'cancelled',
|
|
147
|
+
message: 'Run was cancelled before the result could be written',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
succeeded = true;
|
|
151
|
+
const ep = capture.captured.endpointCount;
|
|
152
|
+
logSuccess(`API docs generated (${ep} endpoint${ep === 1 ? '' : 's'}${capture.captured.openapi ? ', with OpenAPI spec' : ''}).`);
|
|
153
|
+
return {
|
|
154
|
+
status: 'success',
|
|
155
|
+
message: `API docs generated (${ep} endpoint${ep === 1 ? '' : 's'}).`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const msg = 'API docs generation ended without a result message';
|
|
159
|
+
await markFailed(supabase, runId, msg);
|
|
160
|
+
return { status: 'error', message: msg };
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
164
|
+
logError(`API docs generation failed: ${errorMessage}`);
|
|
165
|
+
await markFailed(supabase, runId, errorMessage);
|
|
166
|
+
return { status: 'error', message: errorMessage };
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
if (succeeded) {
|
|
170
|
+
cleanupIssueRepo(repoPath);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// DB helpers — exported for unit tests
|
|
176
|
+
// ============================================================================
|
|
177
|
+
/**
|
|
178
|
+
* Claim the row by flipping `pending` → `running`. Returns true on success and
|
|
179
|
+
* false when the row has already moved on (e.g. user cancelled before the CLI
|
|
180
|
+
* started). Bounded by the status filter so we can't resurrect a 'cancelled'
|
|
181
|
+
* row.
|
|
182
|
+
*/
|
|
183
|
+
export async function markRunning(supabase, runId) {
|
|
184
|
+
const { data, error } = await supabase
|
|
185
|
+
.from('api_doc_runs')
|
|
186
|
+
.update({
|
|
187
|
+
status: 'running',
|
|
188
|
+
error: null,
|
|
189
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
190
|
+
})
|
|
191
|
+
.eq('id', runId)
|
|
192
|
+
.in('status', ['pending', 'running'])
|
|
193
|
+
.select('id')
|
|
194
|
+
.maybeSingle();
|
|
195
|
+
if (error) {
|
|
196
|
+
logWarning(`Could not mark run as running: ${error.message}`);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return data !== null;
|
|
200
|
+
}
|
|
201
|
+
export async function heartbeat(supabase, runId) {
|
|
202
|
+
const { error } = await supabase
|
|
203
|
+
.from('api_doc_runs')
|
|
204
|
+
.update({ last_heartbeat_at: new Date().toISOString() })
|
|
205
|
+
.eq('id', runId)
|
|
206
|
+
.eq('status', 'running');
|
|
207
|
+
if (error) {
|
|
208
|
+
logWarning(`Heartbeat failed: ${error.message}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export async function markFailed(supabase, runId, errorMessage) {
|
|
212
|
+
const { data, error } = await supabase
|
|
213
|
+
.from('api_doc_runs')
|
|
214
|
+
.update({
|
|
215
|
+
status: 'failed',
|
|
216
|
+
error: errorMessage,
|
|
217
|
+
completed_at: new Date().toISOString(),
|
|
218
|
+
})
|
|
219
|
+
.eq('id', runId)
|
|
220
|
+
.in('status', ['pending', 'running'])
|
|
221
|
+
.select('id')
|
|
222
|
+
.maybeSingle();
|
|
223
|
+
if (error) {
|
|
224
|
+
logWarning(`Could not mark run as failed: ${error.message}`);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return data !== null;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Write the captured artifact AND flip the row to success in one update,
|
|
231
|
+
* bounded by the {pending, running} status filter so a cancelled run no-ops.
|
|
232
|
+
*/
|
|
233
|
+
export async function persistAndSucceed(supabase, runId, artifact) {
|
|
234
|
+
const { data, error } = await supabase
|
|
235
|
+
.from('api_doc_runs')
|
|
236
|
+
.update({
|
|
237
|
+
status: 'success',
|
|
238
|
+
error: null,
|
|
239
|
+
openapi: artifact.openapi,
|
|
240
|
+
markdown: artifact.markdown,
|
|
241
|
+
summary: artifact.summary,
|
|
242
|
+
endpoint_count: artifact.endpointCount,
|
|
243
|
+
completed_at: new Date().toISOString(),
|
|
244
|
+
})
|
|
245
|
+
.eq('id', runId)
|
|
246
|
+
.in('status', ['pending', 'running'])
|
|
247
|
+
.select('id')
|
|
248
|
+
.maybeSingle();
|
|
249
|
+
if (error) {
|
|
250
|
+
logWarning(`Could not persist API docs: ${error.message}`);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return data !== null;
|
|
254
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process MCP server exposing a single api-docs tool:
|
|
3
|
+
*
|
|
4
|
+
* - submit_api_docs capture the generated API reference (Markdown + an
|
|
5
|
+
* optional OpenAPI document + summary + endpoint count)
|
|
6
|
+
*
|
|
7
|
+
* Unlike the recipes toolkit (which commits each mutation as it goes) the
|
|
8
|
+
* api-docs artifact is a single document, so the tool just captures into an
|
|
9
|
+
* in-memory buffer. The orchestrator persists the captured artifact and flips
|
|
10
|
+
* the run row to success once the agent's turn ends — a clean all-or-nothing
|
|
11
|
+
* write that can't leave a half-written reference behind.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { type ApiDocsArtifact } from './types.js';
|
|
15
|
+
export interface ApiDocsCaptureState {
|
|
16
|
+
captured: ApiDocsArtifact | null;
|
|
17
|
+
}
|
|
18
|
+
export declare function createApiDocsCaptureState(): ApiDocsCaptureState;
|
|
19
|
+
export declare function createSubmitApiDocsTool(capture: ApiDocsCaptureState): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
|
|
20
|
+
markdown: z.ZodString;
|
|
21
|
+
summary: z.ZodString;
|
|
22
|
+
endpoint_count: z.ZodNumber;
|
|
23
|
+
openapi_json: z.ZodOptional<z.ZodString>;
|
|
24
|
+
}>;
|
|
25
|
+
export declare function createApiDocsMcpServer(capture: ApiDocsCaptureState): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process MCP server exposing a single api-docs tool:
|
|
3
|
+
*
|
|
4
|
+
* - submit_api_docs capture the generated API reference (Markdown + an
|
|
5
|
+
* optional OpenAPI document + summary + endpoint count)
|
|
6
|
+
*
|
|
7
|
+
* Unlike the recipes toolkit (which commits each mutation as it goes) the
|
|
8
|
+
* api-docs artifact is a single document, so the tool just captures into an
|
|
9
|
+
* in-memory buffer. The orchestrator persists the captured artifact and flips
|
|
10
|
+
* the run row to success once the agent's turn ends — a clean all-or-nothing
|
|
11
|
+
* write that can't leave a half-written reference behind.
|
|
12
|
+
*/
|
|
13
|
+
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { API_DOCS_MARKDOWN_MAX, API_DOCS_SUMMARY_MAX, } from './types.js';
|
|
16
|
+
export function createApiDocsCaptureState() {
|
|
17
|
+
return { captured: null };
|
|
18
|
+
}
|
|
19
|
+
function textError(message) {
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: 'text', text: message }],
|
|
22
|
+
isError: true,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function textOk(message) {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: message }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function createSubmitApiDocsTool(capture) {
|
|
31
|
+
return tool('submit_api_docs', 'Submit the final API reference for this repository. Call this exactly once, at the end, after you have explored the codebase. Provide a Markdown reference (always) and, when the repo exposes an HTTP API, a complete OpenAPI 3.x document as a JSON string.', {
|
|
32
|
+
markdown: z
|
|
33
|
+
.string()
|
|
34
|
+
.min(1)
|
|
35
|
+
.max(API_DOCS_MARKDOWN_MAX)
|
|
36
|
+
.describe('The human-readable API reference in Markdown. Document each endpoint / exported interface: method + path (or signature), purpose, parameters, request/response shapes, auth, and an example where useful.'),
|
|
37
|
+
summary: z
|
|
38
|
+
.string()
|
|
39
|
+
.min(1)
|
|
40
|
+
.max(API_DOCS_SUMMARY_MAX)
|
|
41
|
+
.describe('One-paragraph overview of the API surface (what it exposes and how it is consumed).'),
|
|
42
|
+
endpoint_count: z
|
|
43
|
+
.number()
|
|
44
|
+
.int()
|
|
45
|
+
.min(0)
|
|
46
|
+
.describe('How many distinct endpoints / operations the reference documents (0 if the repo exposes no callable API surface).'),
|
|
47
|
+
openapi_json: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('A complete OpenAPI 3.x document serialized as a JSON string. Omit when the repository exposes no HTTP API (e.g. a library or CLI).'),
|
|
51
|
+
}, async (args) => {
|
|
52
|
+
let openapi = null;
|
|
53
|
+
if (args.openapi_json && args.openapi_json.trim()) {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(args.openapi_json);
|
|
56
|
+
if (typeof parsed !== 'object' ||
|
|
57
|
+
parsed === null ||
|
|
58
|
+
Array.isArray(parsed)) {
|
|
59
|
+
return textError('openapi_json must be a JSON object (an OpenAPI document), not an array or scalar.');
|
|
60
|
+
}
|
|
61
|
+
openapi = parsed;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return textError(`openapi_json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
capture.captured = {
|
|
68
|
+
openapi,
|
|
69
|
+
markdown: args.markdown,
|
|
70
|
+
summary: args.summary.trim(),
|
|
71
|
+
endpointCount: args.endpoint_count,
|
|
72
|
+
};
|
|
73
|
+
return textOk(`API docs captured (${args.endpoint_count} endpoint${args.endpoint_count === 1 ? '' : 's'}${openapi ? ', with OpenAPI spec' : ''}). You can end your turn now.`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function createApiDocsMcpServer(capture) {
|
|
77
|
+
return createSdkMcpServer({
|
|
78
|
+
name: 'api-docs',
|
|
79
|
+
version: '1.0.0',
|
|
80
|
+
tools: [createSubmitApiDocsTool(capture)],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompts for the api-docs phase.
|
|
3
|
+
*
|
|
4
|
+
* Agent's job: explore the cloned repository, work out what API surface it
|
|
5
|
+
* exposes (HTTP/REST routes, RPC handlers, edge functions, exported SDK
|
|
6
|
+
* functions, CLI commands — whatever the repo actually offers), and produce
|
|
7
|
+
* an API reference. Output is submitted via the single `submit_api_docs` MCP
|
|
8
|
+
* tool — no fenced JSON, no chat summary.
|
|
9
|
+
*/
|
|
10
|
+
export interface ApiDocsPromptContext {
|
|
11
|
+
repoName: string;
|
|
12
|
+
repoDescription?: string;
|
|
13
|
+
guidance?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function createApiDocsSystemPrompt(): string;
|
|
16
|
+
export declare function createApiDocsUserPrompt(ctx: ApiDocsPromptContext): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompts for the api-docs phase.
|
|
3
|
+
*
|
|
4
|
+
* Agent's job: explore the cloned repository, work out what API surface it
|
|
5
|
+
* exposes (HTTP/REST routes, RPC handlers, edge functions, exported SDK
|
|
6
|
+
* functions, CLI commands — whatever the repo actually offers), and produce
|
|
7
|
+
* an API reference. Output is submitted via the single `submit_api_docs` MCP
|
|
8
|
+
* tool — no fenced JSON, no chat summary.
|
|
9
|
+
*/
|
|
10
|
+
export function createApiDocsSystemPrompt() {
|
|
11
|
+
return `You are a senior engineer writing the API reference for a codebase.
|
|
12
|
+
|
|
13
|
+
The current working directory is a fresh clone of the repository. Use Glob/Grep/Read (and Bash for git/log when helpful) to explore it before writing anything.
|
|
14
|
+
|
|
15
|
+
## Step 1 — Determine the API surface
|
|
16
|
+
|
|
17
|
+
Repositories expose their API in different ways. Figure out which apply here (a repo may have more than one):
|
|
18
|
+
- **HTTP / REST** — route definitions, controllers, request handlers (Express/Fastify/Next.js route handlers, Flask/FastAPI/Django, Spring, Go net/http, etc.).
|
|
19
|
+
- **Action-routed / RPC functions** — e.g. serverless / edge functions dispatched by an \`action\` field on the body, gRPC services, tRPC routers.
|
|
20
|
+
- **Library / SDK** — the public, exported functions/classes other code imports. Document the exported surface, not internal helpers.
|
|
21
|
+
- **CLI** — the commands and flags the tool exposes.
|
|
22
|
+
|
|
23
|
+
Do NOT assume it is REST. Read the code and document what is actually there.
|
|
24
|
+
|
|
25
|
+
## Step 2 — Produce two artifacts
|
|
26
|
+
|
|
27
|
+
1. **Markdown reference (always).** A clear, navigable reference. For each endpoint / operation / exported function document: its identifier (method + path, or signature), purpose, parameters (name, type, required, description), request body and response shapes, auth/permissions, and a short example where it helps. Group by resource/module. Note relative file paths so readers can jump to the source.
|
|
28
|
+
|
|
29
|
+
2. **OpenAPI 3.x document (only when the repo exposes an HTTP API).** A complete, spec-valid OpenAPI document covering the HTTP endpoints — paths, methods, parameters, requestBody/response schemas under \`components.schemas\`, and security schemes. Omit this entirely for pure libraries or CLIs (there is nothing meaningful to put in an OpenAPI \`paths\` object).
|
|
30
|
+
|
|
31
|
+
## Output protocol
|
|
32
|
+
|
|
33
|
+
Submit your result with the \`submit_api_docs\` MCP tool — call it EXACTLY ONCE at the end:
|
|
34
|
+
|
|
35
|
+
- \`markdown\` — the full Markdown reference.
|
|
36
|
+
- \`summary\` — one paragraph describing the API surface.
|
|
37
|
+
- \`endpoint_count\` — the number of distinct endpoints/operations you documented (0 for a non-callable surface).
|
|
38
|
+
- \`openapi_json\` — the OpenAPI document as a JSON string, or omit it when there is no HTTP API.
|
|
39
|
+
|
|
40
|
+
Do NOT paste the reference into chat. Do NOT wrap it in fenced code blocks in a message. Explore, then call the tool, then end your turn.
|
|
41
|
+
|
|
42
|
+
## Rules
|
|
43
|
+
|
|
44
|
+
- Ground everything in the actual source. Don't invent endpoints, parameters, or response fields. If something is ambiguous, describe what the code does and say so.
|
|
45
|
+
- Keep the Markdown focused on the externally-callable surface. Don't document private/internal helpers as if they were API.
|
|
46
|
+
- Prefer accuracy over completeness padding: a small repo gets a short, correct reference.`;
|
|
47
|
+
}
|
|
48
|
+
export function createApiDocsUserPrompt(ctx) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
lines.push(`# Repository: ${ctx.repoName}`);
|
|
51
|
+
if (ctx.repoDescription) {
|
|
52
|
+
lines.push('');
|
|
53
|
+
lines.push('## Description');
|
|
54
|
+
lines.push(ctx.repoDescription);
|
|
55
|
+
}
|
|
56
|
+
if (ctx.guidance && ctx.guidance.trim()) {
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push('## Reviewer guidance (focus or exclusions)');
|
|
59
|
+
lines.push(ctx.guidance.trim());
|
|
60
|
+
}
|
|
61
|
+
lines.push('');
|
|
62
|
+
lines.push('## Task');
|
|
63
|
+
lines.push('Explore the cloned repository, determine what API surface it exposes, and write its API reference. Then call `submit_api_docs` exactly once with the Markdown reference, a one-paragraph summary, the endpoint count, and (only if the repo exposes an HTTP API) the OpenAPI document as a JSON string. End your turn after submitting.');
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|