@openpome/local-gateway 0.34.0-alpha.0 → 0.37.0-alpha.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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +420 -46
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ const maxWorkspaceScanRepositories = 200;
|
|
|
40
40
|
export function getGatewayHealth() {
|
|
41
41
|
return {
|
|
42
42
|
status: "ok",
|
|
43
|
-
version: "0.
|
|
43
|
+
version: "0.37.0-alpha.0"
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
export async function initOpenPome() {
|
|
@@ -231,6 +231,11 @@ export async function runDoctor(env = process.env) {
|
|
|
231
231
|
name: "Model provider",
|
|
232
232
|
status: activeModel?.configured ? "ok" : "attention",
|
|
233
233
|
detail: activeModel?.detail ?? "Run `pome auth ai status` to inspect AI setup."
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "Telemetry",
|
|
237
|
+
status: "ok",
|
|
238
|
+
detail: "Disabled by default. OpenPome does not send analytics, prompts, source code, diffs, or crash data."
|
|
234
239
|
}
|
|
235
240
|
];
|
|
236
241
|
return {
|
|
@@ -534,9 +539,11 @@ export async function getAssistantDecision() {
|
|
|
534
539
|
if (!status.aiPatchProposal && !status.diffSummary) {
|
|
535
540
|
const model = await getModelProviderStatus();
|
|
536
541
|
const activeModel = model.providers.find((provider) => provider.active);
|
|
537
|
-
const
|
|
542
|
+
const aiCanProposePatch = activeModel?.provider !== "manual-copy" && Boolean(activeModel?.configured);
|
|
543
|
+
const blockers = aiCanProposePatch ? collectPlanReadinessWarnings(status) : [
|
|
538
544
|
activeModel?.detail ?? "No AI provider is active.",
|
|
539
|
-
"Connect Claude CLI, Claude API, or OpenAI
|
|
545
|
+
"Connect Claude CLI, Claude API, or OpenAI before AI patch proposals.",
|
|
546
|
+
"Run `pome auth ai claude-cli`, `pome auth ai claude`, or `pome auth ai openai`."
|
|
540
547
|
];
|
|
541
548
|
return buildAssistantDecision(status, "propose_patch", "Ask AI for the smallest safe patch", "OpenPome will collect bounded repo context, ask the active AI provider for changes, and prepare an approval checkpoint.", [
|
|
542
549
|
"pome next",
|
|
@@ -1053,7 +1060,7 @@ export async function discoverTestCommands() {
|
|
|
1053
1060
|
}
|
|
1054
1061
|
const workspace = persisted.workspaceCandidate?.workspace;
|
|
1055
1062
|
const discoveredAt = new Date().toISOString();
|
|
1056
|
-
const candidates = workspace?.path ? await discoverTestCommandCandidates(workspace.path) : getFallbackTestCommandCandidates();
|
|
1063
|
+
const candidates = workspace?.path ? await discoverTestCommandCandidates(workspace.path, persisted) : getFallbackTestCommandCandidates();
|
|
1057
1064
|
await writeActiveTaskSession(paths.homeDirectory, {
|
|
1058
1065
|
...persisted,
|
|
1059
1066
|
testCommandCandidates: candidates,
|
|
@@ -1083,7 +1090,7 @@ export async function approveTestCommand(command) {
|
|
|
1083
1090
|
const candidates = persisted.testCommandCandidates?.length
|
|
1084
1091
|
? persisted.testCommandCandidates
|
|
1085
1092
|
: persisted.workspaceCandidate?.workspace.path
|
|
1086
|
-
? await discoverTestCommandCandidates(persisted.workspaceCandidate.workspace.path)
|
|
1093
|
+
? await discoverTestCommandCandidates(persisted.workspaceCandidate.workspace.path, persisted)
|
|
1087
1094
|
: getFallbackTestCommandCandidates();
|
|
1088
1095
|
const selected = selectTestCommandCandidate(candidates, command);
|
|
1089
1096
|
if (!selected) {
|
|
@@ -1311,16 +1318,16 @@ export async function createGitHubDeviceLogin(env = process.env) {
|
|
|
1311
1318
|
client_id: clientId,
|
|
1312
1319
|
scope
|
|
1313
1320
|
});
|
|
1314
|
-
const response = await
|
|
1321
|
+
const response = await fetchGitHub("https://github.com/login/device/code", {
|
|
1315
1322
|
method: "POST",
|
|
1316
1323
|
headers: {
|
|
1317
1324
|
Accept: "application/json",
|
|
1318
1325
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
1319
1326
|
},
|
|
1320
1327
|
body
|
|
1321
|
-
});
|
|
1328
|
+
}, "start browser login");
|
|
1322
1329
|
if (!response.ok) {
|
|
1323
|
-
throw new Error(
|
|
1330
|
+
throw new Error(await getGitHubStatusGuidance(response, "start browser login"));
|
|
1324
1331
|
}
|
|
1325
1332
|
const payload = (await response.json());
|
|
1326
1333
|
if (!payload.device_code || !payload.user_code || !payload.verification_uri) {
|
|
@@ -1484,7 +1491,7 @@ export async function createPullRequest(options = {}) {
|
|
|
1484
1491
|
}
|
|
1485
1492
|
await runGitStrict(workspacePath, ["add", "-A"]);
|
|
1486
1493
|
await runGitStrict(workspacePath, ["commit", "-m", commitMessage]);
|
|
1487
|
-
await
|
|
1494
|
+
await pushPullRequestBranch(workspacePath, branch);
|
|
1488
1495
|
const storedGitHubToken = github.tokenSource === "openpome" ? await readStoredGitHubOAuth() : undefined;
|
|
1489
1496
|
const prProvider = storedGitHubToken?.accessToken ? "github-api" : "github-cli";
|
|
1490
1497
|
const prUrl = storedGitHubToken?.accessToken
|
|
@@ -1875,15 +1882,15 @@ async function isGitHubCliAuthenticated() {
|
|
|
1875
1882
|
}
|
|
1876
1883
|
}
|
|
1877
1884
|
async function fetchGitHubAuthenticatedUser(accessToken) {
|
|
1878
|
-
const response = await
|
|
1885
|
+
const response = await fetchGitHub("https://api.github.com/user", {
|
|
1879
1886
|
headers: {
|
|
1880
1887
|
Accept: "application/vnd.github+json",
|
|
1881
1888
|
Authorization: `Bearer ${accessToken}`,
|
|
1882
1889
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
1883
1890
|
}
|
|
1884
|
-
});
|
|
1891
|
+
}, "verify authenticated user");
|
|
1885
1892
|
if (!response.ok) {
|
|
1886
|
-
throw new Error(
|
|
1893
|
+
throw new Error(await getGitHubStatusGuidance(response, "verify authenticated user"));
|
|
1887
1894
|
}
|
|
1888
1895
|
const payload = (await response.json());
|
|
1889
1896
|
if (!payload.login) {
|
|
@@ -1896,7 +1903,7 @@ async function fetchGitHubAuthenticatedUser(accessToken) {
|
|
|
1896
1903
|
};
|
|
1897
1904
|
}
|
|
1898
1905
|
async function pollGitHubDeviceAccessToken(clientId, deviceCode) {
|
|
1899
|
-
const response = await
|
|
1906
|
+
const response = await fetchGitHub("https://github.com/login/oauth/access_token", {
|
|
1900
1907
|
method: "POST",
|
|
1901
1908
|
headers: {
|
|
1902
1909
|
Accept: "application/json",
|
|
@@ -1907,11 +1914,11 @@ async function pollGitHubDeviceAccessToken(clientId, deviceCode) {
|
|
|
1907
1914
|
device_code: deviceCode,
|
|
1908
1915
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
1909
1916
|
})
|
|
1910
|
-
});
|
|
1917
|
+
}, "complete browser login");
|
|
1911
1918
|
if (!response.ok) {
|
|
1912
1919
|
return {
|
|
1913
1920
|
status: "error",
|
|
1914
|
-
detail:
|
|
1921
|
+
detail: await getGitHubStatusGuidance(response, "complete browser login")
|
|
1915
1922
|
};
|
|
1916
1923
|
}
|
|
1917
1924
|
const payload = (await response.json());
|
|
@@ -1949,6 +1956,48 @@ function parseGitHubScopes(scope) {
|
|
|
1949
1956
|
.map((value) => value.trim())
|
|
1950
1957
|
.filter((value) => value.length > 0);
|
|
1951
1958
|
}
|
|
1959
|
+
async function fetchGitHub(input, init, action) {
|
|
1960
|
+
try {
|
|
1961
|
+
return await fetch(input, init);
|
|
1962
|
+
}
|
|
1963
|
+
catch (error) {
|
|
1964
|
+
throw new Error(getGitHubNetworkGuidance(action, error));
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
async function getGitHubStatusGuidance(response, action) {
|
|
1968
|
+
const body = await safeResponseText(response);
|
|
1969
|
+
const detail = body ? ` Detail: ${summarizeProviderBody(body)}` : "";
|
|
1970
|
+
if (response.status === 401) {
|
|
1971
|
+
return `GitHub ${action} was unauthorized (401). Run \`pome auth github login\` again, or \`gh auth login\` if you use the GitHub CLI fallback.${detail}`;
|
|
1972
|
+
}
|
|
1973
|
+
if (response.status === 403) {
|
|
1974
|
+
return `GitHub ${action} was forbidden (403). Check repository permission, organization SSO, token scopes, branch protection, and whether the token has \`repo\` access.${detail}`;
|
|
1975
|
+
}
|
|
1976
|
+
if (response.status === 404) {
|
|
1977
|
+
return `GitHub ${action} could not find the repository or resource (404). Check the git remote, repository visibility, GitHub Enterprise host, and account access.${detail}`;
|
|
1978
|
+
}
|
|
1979
|
+
if (response.status === 422) {
|
|
1980
|
+
return `GitHub ${action} was rejected (422). Check whether the branch already has an open PR, base/head branch names are valid, and the repo allows PRs.${detail}`;
|
|
1981
|
+
}
|
|
1982
|
+
if (response.status === 429 || response.headers.get("x-ratelimit-remaining") === "0") {
|
|
1983
|
+
const reset = response.headers.get("x-ratelimit-reset");
|
|
1984
|
+
const resetDetail = reset ? ` Rate limit resets at ${new Date(Number(reset) * 1000).toISOString()}.` : "";
|
|
1985
|
+
return `GitHub rate limit reached while trying to ${action}.${resetDetail} Wait and retry, or use a token with the right organization access.${detail}`;
|
|
1986
|
+
}
|
|
1987
|
+
if (response.status >= 500) {
|
|
1988
|
+
return `GitHub ${action} failed with ${response.status} ${response.statusText}. GitHub may be unavailable, blocked by a proxy, or unreachable from this network.${detail}`;
|
|
1989
|
+
}
|
|
1990
|
+
return `GitHub ${action} failed: ${response.status} ${response.statusText}.${detail}`;
|
|
1991
|
+
}
|
|
1992
|
+
function getGitHubNetworkGuidance(action, error) {
|
|
1993
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1994
|
+
return [
|
|
1995
|
+
`GitHub ${action} could not reach GitHub.`,
|
|
1996
|
+
"Check internet access, VPN split tunneling, proxy/firewall rules, corporate certificate trust, and GitHub Enterprise host configuration.",
|
|
1997
|
+
"Run `pome auth github status` after fixing network access.",
|
|
1998
|
+
`Detail: ${detail}`
|
|
1999
|
+
].join(" ");
|
|
2000
|
+
}
|
|
1952
2001
|
function summarizeUnknownError(error) {
|
|
1953
2002
|
return error instanceof Error ? error.message : String(error);
|
|
1954
2003
|
}
|
|
@@ -1997,7 +2046,11 @@ function getOpenPomePaths() {
|
|
|
1997
2046
|
async function readConfigIfPresent(configFile) {
|
|
1998
2047
|
try {
|
|
1999
2048
|
const content = await readFile(configFile, "utf8");
|
|
2000
|
-
return
|
|
2049
|
+
return {
|
|
2050
|
+
...defaultConfig,
|
|
2051
|
+
...JSON.parse(content),
|
|
2052
|
+
telemetryEnabled: false
|
|
2053
|
+
};
|
|
2001
2054
|
}
|
|
2002
2055
|
catch (error) {
|
|
2003
2056
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -2580,28 +2633,39 @@ function selectArchivedTaskSession(sessions, sessionId) {
|
|
|
2580
2633
|
}
|
|
2581
2634
|
function buildPlanningContext(session) {
|
|
2582
2635
|
const workspace = session.workspaceCandidate?.workspace;
|
|
2636
|
+
const missingRequirementSignals = detectMissingRequirementSignals(session.workItem);
|
|
2583
2637
|
const context = [
|
|
2584
2638
|
`Work item type: ${session.workItem.type}`,
|
|
2585
2639
|
`Status: ${session.workItem.status}`,
|
|
2586
2640
|
session.workItem.priority ? `Priority: ${session.workItem.priority}` : undefined,
|
|
2641
|
+
session.workItem.description ? `Description length: ${session.workItem.description.length} characters` : "Description: not provided",
|
|
2587
2642
|
hasExplicitAcceptanceCriteria(session.workItem)
|
|
2588
2643
|
? "Acceptance criteria: detected in work item text"
|
|
2589
2644
|
: "Acceptance criteria: not explicit; identify missing acceptance criteria before implementation",
|
|
2645
|
+
missingRequirementSignals.length ? `Missing requirement signals: ${missingRequirementSignals.join("; ")}` : undefined,
|
|
2590
2646
|
session.workItem.labels?.length ? `Labels: ${session.workItem.labels.join(", ")}` : undefined,
|
|
2591
2647
|
session.workItem.components?.length ? `Components: ${session.workItem.components.join(", ")}` : undefined,
|
|
2648
|
+
session.workItem.links?.length ? `Linked references: ${session.workItem.links.map((link) => `${link.kind}:${link.title ?? link.url}`).join("; ")}` : undefined,
|
|
2649
|
+
session.workItem.subtasks?.length ? `Subtasks: ${session.workItem.subtasks.map((subtask) => `${subtask.key} ${subtask.status} ${subtask.title}`).join("; ")}` : undefined,
|
|
2592
2650
|
workspace ? `Workspace: ${workspace.name}` : "Workspace: unresolved",
|
|
2593
2651
|
workspace?.path ? `Workspace path: ${workspace.path}` : undefined,
|
|
2594
2652
|
session.workspaceCandidate ? `Workspace confidence: ${Math.round(session.workspaceCandidate.confidence * 100)}%` : undefined,
|
|
2595
|
-
session.workspaceCandidate?.reasons.length ? `Workspace reasons: ${session.workspaceCandidate.reasons.join("; ")}` : undefined
|
|
2653
|
+
session.workspaceCandidate?.reasons.length ? `Workspace reasons: ${session.workspaceCandidate.reasons.join("; ")}` : undefined,
|
|
2654
|
+
workspace?.packageNames?.length ? `Workspace packages: ${workspace.packageNames.slice(0, 8).join(", ")}` : undefined,
|
|
2655
|
+
workspace?.readmeKeywords?.length ? `README signals: ${workspace.readmeKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
2656
|
+
workspace?.codeownersKeywords?.length ? `Ownership signals: ${workspace.codeownersKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
2657
|
+
workspace?.recentBranches?.length ? `Recent branches: ${workspace.recentBranches.slice(0, 8).join(", ")}` : undefined,
|
|
2658
|
+
workspace?.recentCommitRefs?.length ? `Recent work refs: ${workspace.recentCommitRefs.slice(0, 12).join(", ")}` : undefined
|
|
2596
2659
|
];
|
|
2597
2660
|
return context.filter((item) => Boolean(item));
|
|
2598
2661
|
}
|
|
2599
2662
|
function buildInitialImplementationPlan(workItem, workspaceCandidate) {
|
|
2600
2663
|
const workspace = workspaceCandidate?.workspace;
|
|
2601
2664
|
const hasWorkspace = Boolean(workspace?.path);
|
|
2665
|
+
const missingRequirementSignals = detectMissingRequirementSignals(workItem);
|
|
2602
2666
|
const missingInfo = [
|
|
2603
2667
|
hasWorkspace ? undefined : "No workspace candidate is selected yet.",
|
|
2604
|
-
|
|
2668
|
+
...missingRequirementSignals
|
|
2605
2669
|
].filter((item) => Boolean(item));
|
|
2606
2670
|
return {
|
|
2607
2671
|
summary: `Prepare implementation for ${workItem.key}: ${workItem.title}`,
|
|
@@ -2647,7 +2711,36 @@ function buildInitialImplementationPlan(workItem, workspaceCandidate) {
|
|
|
2647
2711
|
}
|
|
2648
2712
|
function hasExplicitAcceptanceCriteria(workItem) {
|
|
2649
2713
|
const text = [workItem.title, workItem.description].filter(Boolean).join("\n").toLowerCase();
|
|
2650
|
-
return /\b(acceptance criteria|acceptance|criteria|given\b.*\bwhen\b.*\bthen|expected result|definition of done|done when|should)\b/su.test(text);
|
|
2714
|
+
return /\b(acceptance criteria|acceptance|criteria|given\b.*\bwhen\b.*\bthen|expected result|definition of done|done when|should|expected behavior|success criteria|verify|validation)\b/su.test(text);
|
|
2715
|
+
}
|
|
2716
|
+
function detectMissingRequirementSignals(workItem) {
|
|
2717
|
+
const text = [workItem.title, workItem.description].filter(Boolean).join("\n").trim();
|
|
2718
|
+
const lower = text.toLowerCase();
|
|
2719
|
+
const signals = [];
|
|
2720
|
+
if (!workItem.description || workItem.description.trim().length < 40) {
|
|
2721
|
+
signals.push("Work item description is short; confirm exact scope before broad edits.");
|
|
2722
|
+
}
|
|
2723
|
+
if (!hasExplicitAcceptanceCriteria(workItem)) {
|
|
2724
|
+
signals.push("Acceptance criteria are not explicit in the work item.");
|
|
2725
|
+
}
|
|
2726
|
+
if (workItem.type === "bug") {
|
|
2727
|
+
const hasExpected = /\b(expected|should happen|desired behavior|correct behavior)\b/u.test(lower);
|
|
2728
|
+
const hasActual = /\b(actual|currently|observed|happens now|error|failure|failed)\b/u.test(lower);
|
|
2729
|
+
const hasRepro = /\b(steps to reproduce|repro|reproduce|given\b.*\bwhen\b.*\bthen)\b/su.test(lower);
|
|
2730
|
+
if (!hasExpected || !hasActual) {
|
|
2731
|
+
signals.push("Bug report is missing clear expected vs actual behavior.");
|
|
2732
|
+
}
|
|
2733
|
+
if (!hasRepro) {
|
|
2734
|
+
signals.push("Bug report has no clear reproduction steps.");
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
if (!workItem.labels?.length && !workItem.components?.length) {
|
|
2738
|
+
signals.push("No labels or components are available to narrow the code area.");
|
|
2739
|
+
}
|
|
2740
|
+
if (!workItem.links?.some((link) => link.kind === "code" || link.kind === "pull_request" || link.kind === "document")) {
|
|
2741
|
+
signals.push("No linked code, pull request, or reference document is attached.");
|
|
2742
|
+
}
|
|
2743
|
+
return Array.from(new Set(signals)).slice(0, 6);
|
|
2651
2744
|
}
|
|
2652
2745
|
async function buildImplementationPlan(persisted, prompt) {
|
|
2653
2746
|
const config = await readConfigIfPresent(getOpenPomePaths().configFile);
|
|
@@ -2674,7 +2767,7 @@ async function completeModelText(provider, prompt) {
|
|
|
2674
2767
|
return completeClaudeCliText(prompt);
|
|
2675
2768
|
}
|
|
2676
2769
|
async function completeOpenAIText(prompt, apiKey) {
|
|
2677
|
-
const response = await
|
|
2770
|
+
const response = await fetchModelProvider("OpenAI", "https://api.openai.com/v1/responses", {
|
|
2678
2771
|
method: "POST",
|
|
2679
2772
|
headers: {
|
|
2680
2773
|
"authorization": `Bearer ${apiKey}`,
|
|
@@ -2686,7 +2779,7 @@ async function completeOpenAIText(prompt, apiKey) {
|
|
|
2686
2779
|
})
|
|
2687
2780
|
});
|
|
2688
2781
|
if (!response.ok) {
|
|
2689
|
-
throw new Error(
|
|
2782
|
+
throw new Error(await getModelProviderStatusGuidance("OpenAI", response, "generate a plan or patch"));
|
|
2690
2783
|
}
|
|
2691
2784
|
const body = await response.json();
|
|
2692
2785
|
if (typeof body.output_text === "string") {
|
|
@@ -2700,7 +2793,7 @@ async function completeOpenAIText(prompt, apiKey) {
|
|
|
2700
2793
|
.join("\n");
|
|
2701
2794
|
}
|
|
2702
2795
|
async function completeAnthropicText(prompt, apiKey) {
|
|
2703
|
-
const response = await
|
|
2796
|
+
const response = await fetchModelProvider("Claude", "https://api.anthropic.com/v1/messages", {
|
|
2704
2797
|
method: "POST",
|
|
2705
2798
|
headers: {
|
|
2706
2799
|
"x-api-key": apiKey,
|
|
@@ -2719,7 +2812,7 @@ async function completeAnthropicText(prompt, apiKey) {
|
|
|
2719
2812
|
})
|
|
2720
2813
|
});
|
|
2721
2814
|
if (!response.ok) {
|
|
2722
|
-
throw new Error(
|
|
2815
|
+
throw new Error(await getModelProviderStatusGuidance("Claude", response, "generate a plan or patch"));
|
|
2723
2816
|
}
|
|
2724
2817
|
const body = await response.json();
|
|
2725
2818
|
const content = Array.isArray(body.content) ? body.content : [];
|
|
@@ -2761,12 +2854,50 @@ async function completeClaudeCliText(prompt) {
|
|
|
2761
2854
|
throw new Error(`Claude CLI request failed: ${summarizeExecError(error) || String(error)}`);
|
|
2762
2855
|
}
|
|
2763
2856
|
}
|
|
2857
|
+
async function fetchModelProvider(provider, input, init) {
|
|
2858
|
+
try {
|
|
2859
|
+
return await fetch(input, init);
|
|
2860
|
+
}
|
|
2861
|
+
catch (error) {
|
|
2862
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2863
|
+
throw new Error(`${provider} could not be reached. Check internet/VPN/proxy access, corporate certificate trust, and provider allowlists. Use \`pome auth ai status\` to verify setup. Detail: ${detail}`);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
async function getModelProviderStatusGuidance(provider, response, action) {
|
|
2867
|
+
const body = await safeResponseText(response);
|
|
2868
|
+
const detail = body ? ` Detail: ${summarizeProviderBody(body)}` : "";
|
|
2869
|
+
const authCommand = provider === "OpenAI" ? "pome auth ai openai" : "pome auth ai claude";
|
|
2870
|
+
if (response.status === 401) {
|
|
2871
|
+
return `${provider} ${action} was unauthorized (401). Reconnect with \`${authCommand}\` or verify the provider API key in your OS credential store/environment.${detail}`;
|
|
2872
|
+
}
|
|
2873
|
+
if (response.status === 403) {
|
|
2874
|
+
return `${provider} ${action} was forbidden (403). Check organization policy, model access, provider project permissions, and corporate egress rules.${detail}`;
|
|
2875
|
+
}
|
|
2876
|
+
if (response.status === 404) {
|
|
2877
|
+
return `${provider} ${action} could not find the configured model or endpoint (404). Check OPENPOME_${provider === "OpenAI" ? "OPENAI_MODEL" : "ANTHROPIC_MODEL"} and provider access.${detail}`;
|
|
2878
|
+
}
|
|
2879
|
+
if (response.status === 408 || response.status === 409 || response.status === 429) {
|
|
2880
|
+
return `${provider} is busy or rate limited (${response.status}). Wait and retry, or choose a smaller model/context. OpenPome has not written files.${detail}`;
|
|
2881
|
+
}
|
|
2882
|
+
if (response.status >= 500) {
|
|
2883
|
+
return `${provider} failed with ${response.status} ${response.statusText}. Provider service may be unavailable or blocked by your network/proxy. Retry later.${detail}`;
|
|
2884
|
+
}
|
|
2885
|
+
return `${provider} ${action} failed: ${response.status} ${response.statusText}.${detail}`;
|
|
2886
|
+
}
|
|
2764
2887
|
function buildStructuredPlanPrompt(prompt) {
|
|
2765
2888
|
return [
|
|
2766
2889
|
"You are OpenPome's planning engine.",
|
|
2890
|
+
"Plan like a senior developer assistant working from a live corporate work item.",
|
|
2891
|
+
"Prefer the smallest repo-aware change that satisfies the work item. Call out unclear scope instead of inventing requirements.",
|
|
2892
|
+
"Use workspace metadata, labels, linked references, ownership signals, and recent branch/commit refs to rank likely files.",
|
|
2893
|
+
"Suggest targeted validation commands before broad commands when the work item points to a specific component.",
|
|
2767
2894
|
"Return only compact JSON with this exact shape:",
|
|
2768
2895
|
"{\"summary\":\"...\",\"assumptions\":[\"...\"],\"steps\":[{\"id\":\"1\",\"title\":\"...\",\"detail\":\"...\"}],\"filesLikelyChanged\":[\"...\"],\"commandsToRun\":[\"...\"],\"risks\":[\"...\"],\"missingInfo\":[\"...\"]}",
|
|
2769
|
-
"
|
|
2896
|
+
"Rules:",
|
|
2897
|
+
"- Do not include source code, full diffs, secrets, or markdown fences.",
|
|
2898
|
+
"- Put missing acceptance criteria, missing repro steps, unclear expected behavior, and missing code links in missingInfo.",
|
|
2899
|
+
"- Keep filesLikelyChanged to relative paths or package/module hints when exact files are unknown.",
|
|
2900
|
+
"- Keep commandsToRun executable from the selected workspace.",
|
|
2770
2901
|
"",
|
|
2771
2902
|
prompt
|
|
2772
2903
|
].join("\n");
|
|
@@ -2825,33 +2956,65 @@ const modelProviderTimeoutMs = 120_000;
|
|
|
2825
2956
|
const modelProviderMaxBufferBytes = 2 * 1024 * 1024;
|
|
2826
2957
|
const sensitivePathFragments = [
|
|
2827
2958
|
".env",
|
|
2959
|
+
".env.local",
|
|
2960
|
+
".env.production",
|
|
2961
|
+
".env.development",
|
|
2828
2962
|
".npmrc",
|
|
2963
|
+
".yarnrc",
|
|
2964
|
+
".pnpmrc",
|
|
2829
2965
|
".pypirc",
|
|
2830
2966
|
".netrc",
|
|
2831
2967
|
".ssh",
|
|
2832
2968
|
".aws",
|
|
2833
2969
|
".gcp",
|
|
2834
2970
|
".azure",
|
|
2971
|
+
".kube",
|
|
2972
|
+
".docker",
|
|
2973
|
+
"credentials",
|
|
2974
|
+
"credential",
|
|
2975
|
+
"secrets",
|
|
2976
|
+
"secret",
|
|
2977
|
+
"private-key",
|
|
2978
|
+
"private_key",
|
|
2835
2979
|
"id_rsa",
|
|
2836
2980
|
"id_dsa",
|
|
2837
|
-
"id_ed25519"
|
|
2981
|
+
"id_ed25519",
|
|
2982
|
+
".pem",
|
|
2983
|
+
".key",
|
|
2984
|
+
".crt",
|
|
2985
|
+
".p12",
|
|
2986
|
+
".pfx"
|
|
2987
|
+
];
|
|
2988
|
+
const sensitiveContentPatterns = [
|
|
2989
|
+
/-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/u,
|
|
2990
|
+
/\b(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|OPENPOME_JIRA_API_TOKEN|OPENPOME_GITHUB_TOKEN|GITHUB_TOKEN|NPM_TOKEN)\s*=\s*['"]?[^'"\s]+/iu,
|
|
2991
|
+
/\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|private[_-]?key)\s*[:=]\s*['"][^'"]{12,}['"]/iu,
|
|
2992
|
+
/\b(?:ghp|github_pat|npm|sk|sk-ant|xox[baprs])-?[A-Za-z0-9_=-]{20,}\b/u
|
|
2838
2993
|
];
|
|
2839
2994
|
async function collectPatchContextFiles(workspacePath, session) {
|
|
2840
2995
|
const candidates = [];
|
|
2841
2996
|
for (const filePath of session.plan?.filesLikelyChanged ?? []) {
|
|
2842
2997
|
const normalized = normalizeWorkspaceRelativePath(workspacePath, filePath, "skip");
|
|
2843
2998
|
if (normalized && normalized !== ".") {
|
|
2844
|
-
candidates.push(
|
|
2999
|
+
candidates.push({
|
|
3000
|
+
filePath: normalized,
|
|
3001
|
+
score: 80,
|
|
3002
|
+
reason: "AI plan marked this file as likely impacted."
|
|
3003
|
+
});
|
|
2845
3004
|
}
|
|
2846
3005
|
}
|
|
2847
|
-
candidates.push("package.json", "README.md", "AGENTS.md");
|
|
3006
|
+
candidates.push({ filePath: "package.json", score: 24, reason: "Package metadata helps infer scripts, package boundaries, and runtime." }, { filePath: "README.md", score: 18, reason: "README gives repository purpose and local validation hints." }, { filePath: "AGENTS.md", score: 18, reason: "Agent instructions constrain safe implementation style." }, { filePath: "CODEOWNERS", score: 14, reason: "Ownership metadata helps identify relevant domains and review paths." });
|
|
2848
3007
|
const trackedFiles = await listTrackedWorkspaceFiles(workspacePath);
|
|
2849
3008
|
const tokens = tokenizePatchSearchText([
|
|
2850
3009
|
session.workItem.key,
|
|
2851
3010
|
session.workItem.title,
|
|
2852
3011
|
session.workItem.description,
|
|
3012
|
+
session.plan?.summary,
|
|
3013
|
+
...(session.plan?.steps.map((step) => `${step.title} ${step.detail ?? ""}`) ?? []),
|
|
2853
3014
|
...(session.workItem.labels ?? []),
|
|
2854
|
-
...(session.workItem.components ?? [])
|
|
3015
|
+
...(session.workItem.components ?? []),
|
|
3016
|
+
...(session.workspaceCandidate?.workspace.packageNames ?? []),
|
|
3017
|
+
...(session.workspaceCandidate?.workspace.readmeKeywords ?? [])
|
|
2855
3018
|
].filter((value) => Boolean(value)).join(" "));
|
|
2856
3019
|
const planHints = new Set((session.plan?.filesLikelyChanged ?? [])
|
|
2857
3020
|
.map((filePath) => normalizeWorkspaceRelativePath(workspacePath, filePath, "skip"))
|
|
@@ -2859,12 +3022,13 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2859
3022
|
const rankedTrackedFiles = trackedFiles
|
|
2860
3023
|
.map((filePath) => ({
|
|
2861
3024
|
filePath,
|
|
2862
|
-
score: scorePatchContextFile(filePath, tokens, planHints)
|
|
3025
|
+
score: scorePatchContextFile(filePath, tokens, planHints),
|
|
3026
|
+
reason: describePatchContextReason(filePath, tokens, planHints)
|
|
2863
3027
|
}))
|
|
2864
3028
|
.filter((candidate) => candidate.score > 0)
|
|
2865
3029
|
.sort((left, right) => right.score - left.score || left.filePath.localeCompare(right.filePath));
|
|
2866
3030
|
for (const candidate of rankedTrackedFiles.slice(0, 40)) {
|
|
2867
|
-
candidates.push(candidate
|
|
3031
|
+
candidates.push(candidate);
|
|
2868
3032
|
}
|
|
2869
3033
|
const selected = [];
|
|
2870
3034
|
const seen = new Set();
|
|
@@ -2873,7 +3037,7 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2873
3037
|
if (selected.length >= maxPatchContextFiles || totalBytes >= maxPatchContextTotalBytes) {
|
|
2874
3038
|
break;
|
|
2875
3039
|
}
|
|
2876
|
-
const relativePath = normalizeWorkspaceRelativePath(workspacePath, candidate, "skip");
|
|
3040
|
+
const relativePath = normalizeWorkspaceRelativePath(workspacePath, candidate.filePath, "skip");
|
|
2877
3041
|
if (!relativePath || seen.has(relativePath) || isSensitiveWorkspacePath(relativePath)) {
|
|
2878
3042
|
continue;
|
|
2879
3043
|
}
|
|
@@ -2881,7 +3045,7 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2881
3045
|
const absolutePath = resolve(workspacePath, relativePath);
|
|
2882
3046
|
try {
|
|
2883
3047
|
const content = await readFile(absolutePath, "utf8");
|
|
2884
|
-
if (content.includes("\u0000")) {
|
|
3048
|
+
if (content.includes("\u0000") || containsSensitiveContent(content)) {
|
|
2885
3049
|
continue;
|
|
2886
3050
|
}
|
|
2887
3051
|
const remainingBytes = maxPatchContextTotalBytes - totalBytes;
|
|
@@ -2891,7 +3055,9 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2891
3055
|
selected.push({
|
|
2892
3056
|
path: relativePath,
|
|
2893
3057
|
content: sliced,
|
|
2894
|
-
truncated: Buffer.byteLength(content, "utf8") > Buffer.byteLength(sliced, "utf8")
|
|
3058
|
+
truncated: Buffer.byteLength(content, "utf8") > Buffer.byteLength(sliced, "utf8"),
|
|
3059
|
+
score: candidate.score,
|
|
3060
|
+
reason: candidate.reason
|
|
2895
3061
|
});
|
|
2896
3062
|
}
|
|
2897
3063
|
catch {
|
|
@@ -2902,12 +3068,45 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2902
3068
|
}
|
|
2903
3069
|
async function listTrackedWorkspaceFiles(workspacePath) {
|
|
2904
3070
|
const output = await runGit(workspacePath, ["ls-files"]);
|
|
2905
|
-
|
|
3071
|
+
const trackedFiles = output
|
|
2906
3072
|
.split(/\r?\n/u)
|
|
2907
3073
|
.map((line) => line.trim())
|
|
2908
3074
|
.filter(Boolean)
|
|
2909
3075
|
.filter((filePath) => !isSensitiveWorkspacePath(filePath))
|
|
2910
3076
|
.slice(0, 1000);
|
|
3077
|
+
return trackedFiles.length > 0 ? trackedFiles : listWorkspaceFilesFallback(workspacePath);
|
|
3078
|
+
}
|
|
3079
|
+
async function listWorkspaceFilesFallback(workspacePath) {
|
|
3080
|
+
const collected = [];
|
|
3081
|
+
const queue = ["."];
|
|
3082
|
+
const ignoredDirectories = new Set([".git", "node_modules", "dist", "build", "coverage", ".next", ".turbo", ".cache", "vendor"]);
|
|
3083
|
+
while (queue.length > 0 && collected.length < 1000) {
|
|
3084
|
+
const current = queue.shift() ?? ".";
|
|
3085
|
+
const absoluteCurrent = resolve(workspacePath, current);
|
|
3086
|
+
let directory;
|
|
3087
|
+
try {
|
|
3088
|
+
directory = await opendir(absoluteCurrent);
|
|
3089
|
+
}
|
|
3090
|
+
catch {
|
|
3091
|
+
continue;
|
|
3092
|
+
}
|
|
3093
|
+
for await (const entry of directory) {
|
|
3094
|
+
const relativePath = current === "." ? entry.name : `${current}/${entry.name}`;
|
|
3095
|
+
if (entry.isDirectory()) {
|
|
3096
|
+
if (!ignoredDirectories.has(entry.name) && !isSensitiveWorkspacePath(relativePath)) {
|
|
3097
|
+
queue.push(relativePath);
|
|
3098
|
+
}
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
if (entry.isFile() && !isSensitiveWorkspacePath(relativePath)) {
|
|
3102
|
+
collected.push(relativePath);
|
|
3103
|
+
if (collected.length >= 1000) {
|
|
3104
|
+
break;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
return collected;
|
|
2911
3110
|
}
|
|
2912
3111
|
function tokenizePatchSearchText(value) {
|
|
2913
3112
|
return Array.from(new Set(value.toLowerCase().split(/[^a-z0-9]+/u).filter((token) => token.length >= 3))).slice(0, 20);
|
|
@@ -2916,7 +3115,7 @@ function scorePatchContextFile(filePath, tokens, planHints) {
|
|
|
2916
3115
|
const lower = filePath.toLowerCase();
|
|
2917
3116
|
let score = 0;
|
|
2918
3117
|
if (planHints.has(filePath)) {
|
|
2919
|
-
score +=
|
|
3118
|
+
score += 40;
|
|
2920
3119
|
}
|
|
2921
3120
|
for (const token of tokens) {
|
|
2922
3121
|
if (lower.includes(token)) {
|
|
@@ -2926,32 +3125,64 @@ function scorePatchContextFile(filePath, tokens, planHints) {
|
|
|
2926
3125
|
if (/\.(ts|tsx|js|jsx|mjs|cjs|py|java|kt|go|rs|rb|php|cs|swift)$/u.test(lower)) {
|
|
2927
3126
|
score += 4;
|
|
2928
3127
|
}
|
|
3128
|
+
if (/(src|app|lib|packages|services|connectors|components|routes|api)\//u.test(lower)) {
|
|
3129
|
+
score += 4;
|
|
3130
|
+
}
|
|
2929
3131
|
if (/(test|spec|__tests__|tests)\b/u.test(lower)) {
|
|
2930
|
-
score +=
|
|
3132
|
+
score += 5;
|
|
2931
3133
|
}
|
|
2932
3134
|
if (/(readme|package\.json|codeowners|agents\.md|tsconfig|vite|jest|vitest|pytest|gradle|pom\.xml|go\.mod|cargo\.toml)/u.test(lower)) {
|
|
2933
3135
|
score += 2;
|
|
2934
3136
|
}
|
|
2935
|
-
if (/(dist|build|coverage|node_modules|vendor|generated|\.lock$|lockfile)/u.test(lower)) {
|
|
2936
|
-
score -=
|
|
3137
|
+
if (/(dist|build|coverage|node_modules|vendor|generated|\.lock$|lockfile|\.min\.)/u.test(lower)) {
|
|
3138
|
+
score -= 16;
|
|
3139
|
+
}
|
|
3140
|
+
if (/(snapshot|snapshots|fixtures|fixture|mock|mocks)\//u.test(lower)) {
|
|
3141
|
+
score -= 2;
|
|
2937
3142
|
}
|
|
2938
3143
|
return score;
|
|
2939
3144
|
}
|
|
3145
|
+
function describePatchContextReason(filePath, tokens, planHints) {
|
|
3146
|
+
const lower = filePath.toLowerCase();
|
|
3147
|
+
const reasons = [
|
|
3148
|
+
planHints.has(filePath) ? "named by the approved plan" : undefined,
|
|
3149
|
+
tokens.filter((token) => lower.includes(token)).slice(0, 4).length
|
|
3150
|
+
? `matches task token(s): ${tokens.filter((token) => lower.includes(token)).slice(0, 4).join(", ")}`
|
|
3151
|
+
: undefined,
|
|
3152
|
+
/(test|spec|__tests__|tests)\b/u.test(lower) ? "is a related validation file" : undefined,
|
|
3153
|
+
/(package\.json|tsconfig|vite|jest|vitest|pytest|gradle|pom\.xml|go\.mod|cargo\.toml)/u.test(lower) ? "contains project or test configuration" : undefined,
|
|
3154
|
+
/(readme|codeowners|agents\.md)/u.test(lower) ? "contains repository guidance" : undefined,
|
|
3155
|
+
/\.(ts|tsx|js|jsx|mjs|cjs|py|java|kt|go|rs|rb|php|cs|swift)$/u.test(lower) ? "is source code in a supported language" : undefined
|
|
3156
|
+
].filter((reason) => Boolean(reason));
|
|
3157
|
+
return reasons.length ? reasons.join("; ") : "ranked from repository metadata and work item text";
|
|
3158
|
+
}
|
|
2940
3159
|
function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
2941
3160
|
const plan = session.plan;
|
|
2942
3161
|
const context = contextFiles.map((file) => [
|
|
2943
3162
|
`FILE: ${file.path}${file.truncated ? " (truncated)" : ""}`,
|
|
3163
|
+
`RANK: ${file.score}`,
|
|
3164
|
+
`WHY_INCLUDED: ${file.reason}`,
|
|
2944
3165
|
"```",
|
|
2945
3166
|
file.content,
|
|
2946
3167
|
"```"
|
|
2947
3168
|
].join("\n")).join("\n\n");
|
|
2948
3169
|
const failedTestContext = getFailedTestContextAfterLatestPatch(session);
|
|
3170
|
+
const missingRequirementSignals = Array.from(new Set([
|
|
3171
|
+
...detectMissingRequirementSignals(session.workItem),
|
|
3172
|
+
...(plan?.missingInfo ?? [])
|
|
3173
|
+
])).slice(0, 8);
|
|
3174
|
+
const workspace = session.workspaceCandidate?.workspace;
|
|
2949
3175
|
return [
|
|
2950
3176
|
"You are OpenPome's implementation engine.",
|
|
3177
|
+
failedTestContext.length
|
|
3178
|
+
? "This is a retry after approved validation failed. Repair only the failure using the evidence below."
|
|
3179
|
+
: "This is the first implementation patch for the approved plan.",
|
|
2951
3180
|
"Return only compact JSON. Do not include markdown fences outside JSON.",
|
|
2952
3181
|
"Only propose a minimal safe file patch for the approved work item.",
|
|
2953
3182
|
"Do not include secrets, credentials, generated dependency folders, lockfile rewrites, or unrelated refactors.",
|
|
2954
3183
|
"Use full replacement file content for each proposed file.",
|
|
3184
|
+
"If requirements are unclear, prefer a small diagnostic or guardrail change over a speculative broad rewrite.",
|
|
3185
|
+
"Keep existing style, imports, formatting, and public contracts unless the work item clearly requires a change.",
|
|
2955
3186
|
"Allowed JSON shape:",
|
|
2956
3187
|
"{\"summary\":\"...\",\"files\":[{\"path\":\"relative/path\",\"action\":\"create|update\",\"content\":\"full file content\"}],\"risks\":[\"...\"]}",
|
|
2957
3188
|
"",
|
|
@@ -2964,7 +3195,11 @@ function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
|
2964
3195
|
session.workItem.priority ? `- Priority: ${session.workItem.priority}` : undefined,
|
|
2965
3196
|
session.workItem.labels?.length ? `- Labels: ${session.workItem.labels.join(", ")}` : undefined,
|
|
2966
3197
|
session.workItem.components?.length ? `- Components: ${session.workItem.components.join(", ")}` : undefined,
|
|
3198
|
+
session.workItem.links?.length ? `- Links: ${session.workItem.links.map((link) => `${link.kind}:${link.title ?? link.url}`).join("; ")}` : undefined,
|
|
2967
3199
|
"",
|
|
3200
|
+
missingRequirementSignals.length ? "Known missing or unclear requirements:" : undefined,
|
|
3201
|
+
...missingRequirementSignals.map((signal) => `- ${signal}`),
|
|
3202
|
+
missingRequirementSignals.length ? "" : undefined,
|
|
2968
3203
|
"Approved plan:",
|
|
2969
3204
|
plan?.summary ? `- Summary: ${plan.summary}` : "- Summary: unavailable",
|
|
2970
3205
|
...(plan?.steps.map((step) => `- ${step.title}${step.detail ? `: ${step.detail}` : ""}`) ?? []),
|
|
@@ -2975,7 +3210,13 @@ function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
|
2975
3210
|
failedTestContext.length ? "" : undefined,
|
|
2976
3211
|
"Workspace:",
|
|
2977
3212
|
`- Path: ${workspacePath}`,
|
|
2978
|
-
|
|
3213
|
+
workspace?.name ? `- Name: ${workspace.name}` : undefined,
|
|
3214
|
+
workspace?.currentBranch ? `- Current branch: ${workspace.currentBranch}` : undefined,
|
|
3215
|
+
workspace?.packageNames?.length ? `- Packages: ${workspace.packageNames.slice(0, 8).join(", ")}` : undefined,
|
|
3216
|
+
workspace?.readmeKeywords?.length ? `- README signals: ${workspace.readmeKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
3217
|
+
workspace?.codeownersKeywords?.length ? `- Ownership signals: ${workspace.codeownersKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
3218
|
+
workspace?.recentBranches?.length ? `- Recent branches: ${workspace.recentBranches.slice(0, 8).join(", ")}` : undefined,
|
|
3219
|
+
workspace?.recentCommitRefs?.length ? `- Recent work refs: ${workspace.recentCommitRefs.slice(0, 12).join(", ")}` : undefined,
|
|
2979
3220
|
"",
|
|
2980
3221
|
"Readable context files:",
|
|
2981
3222
|
context || "No source files were safely included. You may propose small new files only if the task clearly asks for them."
|
|
@@ -3022,7 +3263,7 @@ function parseAIPatchProposal(value, session, provider, workspacePath, createdAt
|
|
|
3022
3263
|
const relativePath = normalizeWorkspaceRelativePath(workspacePath, maybe.path, "throw");
|
|
3023
3264
|
const action = maybe.action === "create" ? "create" : "update";
|
|
3024
3265
|
const content = maybe.content;
|
|
3025
|
-
if (!relativePath || isSensitiveWorkspacePath(relativePath) || content.includes("\u0000")) {
|
|
3266
|
+
if (!relativePath || isSensitiveWorkspacePath(relativePath) || content.includes("\u0000") || containsSensitiveContent(content)) {
|
|
3026
3267
|
return undefined;
|
|
3027
3268
|
}
|
|
3028
3269
|
if (Buffer.byteLength(content, "utf8") > maxPatchProposalBytesPerFile) {
|
|
@@ -3070,6 +3311,9 @@ function isSensitiveWorkspacePath(filePath) {
|
|
|
3070
3311
|
}
|
|
3071
3312
|
return sensitivePathFragments.some((fragment) => lower === fragment || lower.includes(`/${fragment}`) || lower.endsWith(fragment));
|
|
3072
3313
|
}
|
|
3314
|
+
function containsSensitiveContent(content) {
|
|
3315
|
+
return sensitiveContentPatterns.some((pattern) => pattern.test(content));
|
|
3316
|
+
}
|
|
3073
3317
|
async function applyPatchFiles(workspacePath, files) {
|
|
3074
3318
|
for (const file of files) {
|
|
3075
3319
|
const relativePath = normalizeWorkspaceRelativePath(workspacePath, file.path, "throw");
|
|
@@ -3081,7 +3325,7 @@ async function applyPatchFiles(workspacePath, files) {
|
|
|
3081
3325
|
await writeFile(absolutePath, file.content, "utf8");
|
|
3082
3326
|
}
|
|
3083
3327
|
}
|
|
3084
|
-
async function discoverTestCommandCandidates(workspacePath) {
|
|
3328
|
+
async function discoverTestCommandCandidates(workspacePath, session) {
|
|
3085
3329
|
const scripts = await readPackageScripts(join(workspacePath, "package.json"));
|
|
3086
3330
|
const packageManager = detectPackageManager(workspacePath);
|
|
3087
3331
|
const candidates = [];
|
|
@@ -3097,6 +3341,17 @@ async function discoverTestCommandCandidates(workspacePath) {
|
|
|
3097
3341
|
cwd: workspacePath
|
|
3098
3342
|
});
|
|
3099
3343
|
}
|
|
3344
|
+
const relatedTestFiles = session ? await findRelatedTestFiles(workspacePath, session) : [];
|
|
3345
|
+
const testScript = scripts["test"] ? buildPackageScriptCommand(packageManager, "test") : undefined;
|
|
3346
|
+
for (const testFile of relatedTestFiles.slice(0, 5)) {
|
|
3347
|
+
candidates.push({
|
|
3348
|
+
id: `related_${createHash("sha256").update(testFile).digest("hex").slice(0, 8)}`,
|
|
3349
|
+
command: testScript ? `${testScript} -- ${quoteShellArg(testFile)}` : buildLanguageSpecificTestCommand(packageManager, testFile),
|
|
3350
|
+
source: "related_file",
|
|
3351
|
+
reason: `Related test file matched likely impacted work: ${testFile}.`,
|
|
3352
|
+
cwd: workspacePath
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3100
3355
|
if (candidates.length > 0) {
|
|
3101
3356
|
return candidates;
|
|
3102
3357
|
}
|
|
@@ -3113,6 +3368,74 @@ async function discoverTestCommandCandidates(workspacePath) {
|
|
|
3113
3368
|
}
|
|
3114
3369
|
return getFallbackTestCommandCandidates(workspacePath);
|
|
3115
3370
|
}
|
|
3371
|
+
async function findRelatedTestFiles(workspacePath, session) {
|
|
3372
|
+
let trackedFiles = [];
|
|
3373
|
+
try {
|
|
3374
|
+
trackedFiles = await listTrackedWorkspaceFiles(workspacePath);
|
|
3375
|
+
}
|
|
3376
|
+
catch {
|
|
3377
|
+
return [];
|
|
3378
|
+
}
|
|
3379
|
+
const impactHints = new Set([
|
|
3380
|
+
...(session.plan?.filesLikelyChanged ?? [])
|
|
3381
|
+
.map((filePath) => normalizeWorkspaceRelativePath(workspacePath, filePath, "skip"))
|
|
3382
|
+
.filter((filePath) => Boolean(filePath))
|
|
3383
|
+
.flatMap((filePath) => [filePath, basename(filePath).replace(/\.[^.]+$/u, "")]),
|
|
3384
|
+
...tokenizePatchSearchText([
|
|
3385
|
+
session.workItem.key,
|
|
3386
|
+
session.workItem.title,
|
|
3387
|
+
session.workItem.description,
|
|
3388
|
+
...(session.workItem.labels ?? []),
|
|
3389
|
+
...(session.workItem.components ?? []),
|
|
3390
|
+
session.plan?.summary
|
|
3391
|
+
].filter((value) => Boolean(value)).join(" "))
|
|
3392
|
+
]);
|
|
3393
|
+
return trackedFiles
|
|
3394
|
+
.filter((filePath) => isTestLikeFile(filePath))
|
|
3395
|
+
.map((filePath) => ({
|
|
3396
|
+
filePath,
|
|
3397
|
+
score: scoreRelatedTestFile(filePath, impactHints)
|
|
3398
|
+
}))
|
|
3399
|
+
.filter((candidate) => candidate.score > 0)
|
|
3400
|
+
.sort((left, right) => right.score - left.score || left.filePath.localeCompare(right.filePath))
|
|
3401
|
+
.map((candidate) => candidate.filePath);
|
|
3402
|
+
}
|
|
3403
|
+
function isTestLikeFile(filePath) {
|
|
3404
|
+
return /(^|\/)(__tests__|tests?|specs?)\//u.test(filePath.toLowerCase())
|
|
3405
|
+
|| /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py|java|kt|go|rs|rb|php|cs)$/u.test(filePath.toLowerCase());
|
|
3406
|
+
}
|
|
3407
|
+
function scoreRelatedTestFile(filePath, impactHints) {
|
|
3408
|
+
const lower = filePath.toLowerCase();
|
|
3409
|
+
let score = 0;
|
|
3410
|
+
for (const hint of impactHints) {
|
|
3411
|
+
const normalized = hint.toLowerCase();
|
|
3412
|
+
if (normalized.length >= 3 && lower.includes(normalized)) {
|
|
3413
|
+
score += normalized.includes("/") ? 10 : 4;
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
if (score === 0) {
|
|
3417
|
+
return 0;
|
|
3418
|
+
}
|
|
3419
|
+
if (/\.(test|spec)\./u.test(lower)) {
|
|
3420
|
+
score += 3;
|
|
3421
|
+
}
|
|
3422
|
+
if (/(__tests__|tests?)\//u.test(lower)) {
|
|
3423
|
+
score += 2;
|
|
3424
|
+
}
|
|
3425
|
+
if (/(snapshot|fixtures|mocks)\//u.test(lower)) {
|
|
3426
|
+
score -= 2;
|
|
3427
|
+
}
|
|
3428
|
+
return score;
|
|
3429
|
+
}
|
|
3430
|
+
function buildLanguageSpecificTestCommand(packageManager, testFile) {
|
|
3431
|
+
if (/\.(py)$/u.test(testFile)) {
|
|
3432
|
+
return `python -m pytest ${quoteShellArg(testFile)}`;
|
|
3433
|
+
}
|
|
3434
|
+
if (/\.(go)$/u.test(testFile)) {
|
|
3435
|
+
return "go test ./...";
|
|
3436
|
+
}
|
|
3437
|
+
return `${buildPackageScriptCommand(packageManager, "test")} -- ${quoteShellArg(testFile)}`;
|
|
3438
|
+
}
|
|
3116
3439
|
function detectPackageManager(workspacePath) {
|
|
3117
3440
|
if (existsSync(join(workspacePath, "pnpm-lock.yaml"))) {
|
|
3118
3441
|
return "pnpm";
|
|
@@ -3137,6 +3460,9 @@ function buildPackageScriptCommand(packageManager, scriptName) {
|
|
|
3137
3460
|
}
|
|
3138
3461
|
return `pnpm ${scriptName}`;
|
|
3139
3462
|
}
|
|
3463
|
+
function quoteShellArg(value) {
|
|
3464
|
+
return `'${value.replace(/'/gu, "'\\''")}'`;
|
|
3465
|
+
}
|
|
3140
3466
|
function getFallbackTestCommandCandidates(cwd) {
|
|
3141
3467
|
return [
|
|
3142
3468
|
{
|
|
@@ -3237,6 +3563,14 @@ async function ensurePullRequestBranch(workspacePath, branch) {
|
|
|
3237
3563
|
}
|
|
3238
3564
|
return branch;
|
|
3239
3565
|
}
|
|
3566
|
+
async function pushPullRequestBranch(workspacePath, branch) {
|
|
3567
|
+
try {
|
|
3568
|
+
await runGitStrict(workspacePath, ["push", "-u", "origin", branch]);
|
|
3569
|
+
}
|
|
3570
|
+
catch (error) {
|
|
3571
|
+
throw new Error(getGitHubCliGuidance("push pull request branch", error));
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3240
3574
|
async function createGitHubPullRequestWithCli(workspacePath, draft, branch, draftPr) {
|
|
3241
3575
|
const ghArgs = [
|
|
3242
3576
|
"pr",
|
|
@@ -3253,14 +3587,19 @@ async function createGitHubPullRequestWithCli(workspacePath, draft, branch, draf
|
|
|
3253
3587
|
if (draftPr) {
|
|
3254
3588
|
ghArgs.push("--draft");
|
|
3255
3589
|
}
|
|
3256
|
-
|
|
3590
|
+
try {
|
|
3591
|
+
return (await execFileStrict("gh", ghArgs, workspacePath)).trim();
|
|
3592
|
+
}
|
|
3593
|
+
catch (error) {
|
|
3594
|
+
throw new Error(getGitHubCliGuidance("create pull request", error));
|
|
3595
|
+
}
|
|
3257
3596
|
}
|
|
3258
3597
|
async function createGitHubPullRequestWithApi(accessToken, draft, branch, draftPr) {
|
|
3259
3598
|
const repository = parseGitHubRepositoryCoordinates(draft.remoteUrl);
|
|
3260
3599
|
if (!repository) {
|
|
3261
3600
|
throw new Error("Unable to determine GitHub owner/repo from the workspace remote URL.");
|
|
3262
3601
|
}
|
|
3263
|
-
const response = await
|
|
3602
|
+
const response = await fetchGitHub(`${getGitHubApiBaseUrl()}/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/pulls`, {
|
|
3264
3603
|
method: "POST",
|
|
3265
3604
|
headers: {
|
|
3266
3605
|
Accept: "application/vnd.github+json",
|
|
@@ -3275,14 +3614,22 @@ async function createGitHubPullRequestWithApi(accessToken, draft, branch, draftP
|
|
|
3275
3614
|
base: draft.baseBranch,
|
|
3276
3615
|
draft: draftPr
|
|
3277
3616
|
})
|
|
3278
|
-
});
|
|
3617
|
+
}, "create pull request");
|
|
3279
3618
|
if (!response.ok) {
|
|
3280
|
-
|
|
3281
|
-
throw new Error(`GitHub PR creation failed: ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`);
|
|
3619
|
+
throw new Error(await getGitHubStatusGuidance(response, "create pull request"));
|
|
3282
3620
|
}
|
|
3283
3621
|
const payload = (await response.json());
|
|
3284
3622
|
return payload.html_url ?? `https://github.com/${repository.owner}/${repository.repo}/pull/${payload.number ?? ""}`;
|
|
3285
3623
|
}
|
|
3624
|
+
function getGitHubCliGuidance(action, error) {
|
|
3625
|
+
const detail = summarizeExecError(error) ?? String(error);
|
|
3626
|
+
return [
|
|
3627
|
+
`GitHub ${action} failed.`,
|
|
3628
|
+
"Check repository write permission, organization SSO authorization, branch protection, remote URL, and whether your token/SSH key can push to origin.",
|
|
3629
|
+
"Run `pome auth github status` and `git remote -v` to verify the account and repository.",
|
|
3630
|
+
`Detail: ${detail}`
|
|
3631
|
+
].join(" ");
|
|
3632
|
+
}
|
|
3286
3633
|
function parseGitHubRepositoryCoordinates(remoteUrl) {
|
|
3287
3634
|
if (!remoteUrl) {
|
|
3288
3635
|
return undefined;
|
|
@@ -3331,6 +3678,33 @@ async function safeResponseText(response) {
|
|
|
3331
3678
|
return "";
|
|
3332
3679
|
}
|
|
3333
3680
|
}
|
|
3681
|
+
function summarizeProviderBody(value) {
|
|
3682
|
+
const trimmed = value.trim();
|
|
3683
|
+
if (!trimmed) {
|
|
3684
|
+
return "";
|
|
3685
|
+
}
|
|
3686
|
+
try {
|
|
3687
|
+
const parsed = JSON.parse(trimmed);
|
|
3688
|
+
const errorObject = typeof parsed.error === "object" && parsed.error
|
|
3689
|
+
? parsed.error
|
|
3690
|
+
: undefined;
|
|
3691
|
+
const messages = [
|
|
3692
|
+
typeof parsed.message === "string" ? parsed.message : undefined,
|
|
3693
|
+
typeof parsed.error === "string" ? parsed.error : undefined,
|
|
3694
|
+
typeof errorObject?.message === "string" ? errorObject.message : undefined,
|
|
3695
|
+
typeof errorObject?.type === "string" ? `type=${errorObject.type}` : undefined,
|
|
3696
|
+
typeof errorObject?.code === "string" ? `code=${errorObject.code}` : undefined,
|
|
3697
|
+
typeof parsed.documentation_url === "string" ? parsed.documentation_url : undefined
|
|
3698
|
+
].filter((item) => Boolean(item));
|
|
3699
|
+
if (messages.length) {
|
|
3700
|
+
return messages.join("; ").slice(0, 500);
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
catch {
|
|
3704
|
+
// Fall through to plain-text summary.
|
|
3705
|
+
}
|
|
3706
|
+
return trimmed.replace(/\s+/gu, " ").slice(0, 500);
|
|
3707
|
+
}
|
|
3334
3708
|
async function hasWorkspaceChanges(workspacePath) {
|
|
3335
3709
|
const output = await runGit(workspacePath, ["status", "--porcelain"]);
|
|
3336
3710
|
return output.trim().length > 0;
|