@openpome/local-gateway 0.34.0-alpha.0 → 0.38.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 +25 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +552 -47
- package/dist/index.js.map +1 -1
- package/package.json +11 -10
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { promisify } from "node:util";
|
|
|
9
9
|
import { defaultConfig } from "@openpome/configuration";
|
|
10
10
|
import { createCredentialStore, getJsonCredential, setJsonCredential } from "@openpome/credentials";
|
|
11
11
|
import { groupWorkItemsByType } from "@openpome/work-items";
|
|
12
|
+
import { getLocalPersistenceInfo, openSessionSnapshotStore } from "@openpome/persistence";
|
|
12
13
|
import { buildPlanningPrompt } from "@openpome/prompt-engine";
|
|
13
14
|
import { rankWorkspaceCandidates } from "@openpome/workspaces";
|
|
14
15
|
import { createDefaultWorkItemSourceRegistry, createJiraCloudOAuthLogin, exchangeJiraCloudOAuthCode, refreshJiraCloudOAuthToken } from "./connectors/work-item-registry.js";
|
|
@@ -40,7 +41,7 @@ const maxWorkspaceScanRepositories = 200;
|
|
|
40
41
|
export function getGatewayHealth() {
|
|
41
42
|
return {
|
|
42
43
|
status: "ok",
|
|
43
|
-
version: "0.
|
|
44
|
+
version: "0.38.0-alpha.0"
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
export async function initOpenPome() {
|
|
@@ -231,6 +232,11 @@ export async function runDoctor(env = process.env) {
|
|
|
231
232
|
name: "Model provider",
|
|
232
233
|
status: activeModel?.configured ? "ok" : "attention",
|
|
233
234
|
detail: activeModel?.detail ?? "Run `pome auth ai status` to inspect AI setup."
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "Telemetry",
|
|
238
|
+
status: "ok",
|
|
239
|
+
detail: "Disabled by default. OpenPome does not send analytics, prompts, source code, diffs, or crash data."
|
|
234
240
|
}
|
|
235
241
|
];
|
|
236
242
|
return {
|
|
@@ -534,9 +540,11 @@ export async function getAssistantDecision() {
|
|
|
534
540
|
if (!status.aiPatchProposal && !status.diffSummary) {
|
|
535
541
|
const model = await getModelProviderStatus();
|
|
536
542
|
const activeModel = model.providers.find((provider) => provider.active);
|
|
537
|
-
const
|
|
543
|
+
const aiCanProposePatch = activeModel?.provider !== "manual-copy" && Boolean(activeModel?.configured);
|
|
544
|
+
const blockers = aiCanProposePatch ? collectPlanReadinessWarnings(status) : [
|
|
538
545
|
activeModel?.detail ?? "No AI provider is active.",
|
|
539
|
-
"Connect Claude CLI, Claude API, or OpenAI
|
|
546
|
+
"Connect Claude CLI, Claude API, or OpenAI before AI patch proposals.",
|
|
547
|
+
"Run `pome auth ai claude-cli`, `pome auth ai claude`, or `pome auth ai openai`."
|
|
540
548
|
];
|
|
541
549
|
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
550
|
"pome next",
|
|
@@ -635,11 +643,13 @@ function getLatestTestRunAfterStatus(status, since) {
|
|
|
635
643
|
export async function stopTaskSession() {
|
|
636
644
|
const paths = getOpenPomePaths();
|
|
637
645
|
const persisted = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
646
|
+
const databaseFile = getSessionDatabaseFile(paths.homeDirectory);
|
|
638
647
|
if (!persisted) {
|
|
639
648
|
return {
|
|
640
649
|
active: false,
|
|
641
650
|
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
642
651
|
historyFile: getTaskSessionHistoryFile(paths.homeDirectory),
|
|
652
|
+
databaseFile,
|
|
643
653
|
message: "No active task session to stop."
|
|
644
654
|
};
|
|
645
655
|
}
|
|
@@ -666,6 +676,7 @@ export async function stopTaskSession() {
|
|
|
666
676
|
active: false,
|
|
667
677
|
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
668
678
|
historyFile: getTaskSessionHistoryFile(paths.homeDirectory),
|
|
679
|
+
databaseFile,
|
|
669
680
|
session,
|
|
670
681
|
message: "Stopped active task session and archived it locally."
|
|
671
682
|
};
|
|
@@ -673,22 +684,25 @@ export async function stopTaskSession() {
|
|
|
673
684
|
export async function resumeTaskSession(sessionId) {
|
|
674
685
|
const paths = getOpenPomePaths();
|
|
675
686
|
const active = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
687
|
+
const databaseFile = getSessionDatabaseFile(paths.homeDirectory);
|
|
676
688
|
if (active) {
|
|
677
689
|
return {
|
|
678
690
|
active: true,
|
|
679
691
|
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
680
692
|
historyFile: getTaskSessionHistoryFile(paths.homeDirectory),
|
|
693
|
+
databaseFile,
|
|
681
694
|
session: active.session,
|
|
682
695
|
message: "Active task session is already available."
|
|
683
696
|
};
|
|
684
697
|
}
|
|
685
698
|
const history = await readTaskSessionHistoryIfPresent(paths.homeDirectory);
|
|
686
|
-
const archived = selectArchivedTaskSession(history?.sessions ?? [], sessionId);
|
|
699
|
+
const archived = selectArchivedTaskSession(await listArchivedTaskSessions(paths.homeDirectory, history?.sessions ?? []), sessionId);
|
|
687
700
|
if (!archived) {
|
|
688
701
|
return {
|
|
689
702
|
active: false,
|
|
690
703
|
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
691
704
|
historyFile: getTaskSessionHistoryFile(paths.homeDirectory),
|
|
705
|
+
databaseFile,
|
|
692
706
|
message: sessionId ? `No archived task session found: ${sessionId}` : "No archived task session is available to resume."
|
|
693
707
|
};
|
|
694
708
|
}
|
|
@@ -714,6 +728,7 @@ export async function resumeTaskSession(sessionId) {
|
|
|
714
728
|
active: true,
|
|
715
729
|
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
716
730
|
historyFile: getTaskSessionHistoryFile(paths.homeDirectory),
|
|
731
|
+
databaseFile,
|
|
717
732
|
session,
|
|
718
733
|
message: "Resumed archived task session."
|
|
719
734
|
};
|
|
@@ -721,11 +736,13 @@ export async function resumeTaskSession(sessionId) {
|
|
|
721
736
|
export async function resetTaskSession() {
|
|
722
737
|
const paths = getOpenPomePaths();
|
|
723
738
|
const persisted = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
739
|
+
const databaseFile = getSessionDatabaseFile(paths.homeDirectory);
|
|
724
740
|
if (!persisted) {
|
|
725
741
|
return {
|
|
726
742
|
active: false,
|
|
727
743
|
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
728
744
|
historyFile: getTaskSessionHistoryFile(paths.homeDirectory),
|
|
745
|
+
databaseFile,
|
|
729
746
|
message: "No active task session to reset."
|
|
730
747
|
};
|
|
731
748
|
}
|
|
@@ -752,10 +769,21 @@ export async function resetTaskSession() {
|
|
|
752
769
|
active: false,
|
|
753
770
|
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
754
771
|
historyFile: getTaskSessionHistoryFile(paths.homeDirectory),
|
|
772
|
+
databaseFile,
|
|
755
773
|
session,
|
|
756
774
|
message: "Reset active task session and archived it locally."
|
|
757
775
|
};
|
|
758
776
|
}
|
|
777
|
+
export async function getTaskSessionHistory(limit = 25) {
|
|
778
|
+
const paths = getOpenPomePaths();
|
|
779
|
+
const history = await readTaskSessionHistoryIfPresent(paths.homeDirectory);
|
|
780
|
+
const sessions = await listArchivedTaskSessions(paths.homeDirectory, history?.sessions ?? [], limit);
|
|
781
|
+
return {
|
|
782
|
+
historyFile: getTaskSessionHistoryFile(paths.homeDirectory),
|
|
783
|
+
databaseFile: getSessionDatabaseFile(paths.homeDirectory),
|
|
784
|
+
sessions: sessions.map(summarizeTaskSessionHistory)
|
|
785
|
+
};
|
|
786
|
+
}
|
|
759
787
|
export async function createTaskSessionPlan() {
|
|
760
788
|
const paths = getOpenPomePaths();
|
|
761
789
|
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
@@ -1053,7 +1081,7 @@ export async function discoverTestCommands() {
|
|
|
1053
1081
|
}
|
|
1054
1082
|
const workspace = persisted.workspaceCandidate?.workspace;
|
|
1055
1083
|
const discoveredAt = new Date().toISOString();
|
|
1056
|
-
const candidates = workspace?.path ? await discoverTestCommandCandidates(workspace.path) : getFallbackTestCommandCandidates();
|
|
1084
|
+
const candidates = workspace?.path ? await discoverTestCommandCandidates(workspace.path, persisted) : getFallbackTestCommandCandidates();
|
|
1057
1085
|
await writeActiveTaskSession(paths.homeDirectory, {
|
|
1058
1086
|
...persisted,
|
|
1059
1087
|
testCommandCandidates: candidates,
|
|
@@ -1083,7 +1111,7 @@ export async function approveTestCommand(command) {
|
|
|
1083
1111
|
const candidates = persisted.testCommandCandidates?.length
|
|
1084
1112
|
? persisted.testCommandCandidates
|
|
1085
1113
|
: persisted.workspaceCandidate?.workspace.path
|
|
1086
|
-
? await discoverTestCommandCandidates(persisted.workspaceCandidate.workspace.path)
|
|
1114
|
+
? await discoverTestCommandCandidates(persisted.workspaceCandidate.workspace.path, persisted)
|
|
1087
1115
|
: getFallbackTestCommandCandidates();
|
|
1088
1116
|
const selected = selectTestCommandCandidate(candidates, command);
|
|
1089
1117
|
if (!selected) {
|
|
@@ -1311,16 +1339,16 @@ export async function createGitHubDeviceLogin(env = process.env) {
|
|
|
1311
1339
|
client_id: clientId,
|
|
1312
1340
|
scope
|
|
1313
1341
|
});
|
|
1314
|
-
const response = await
|
|
1342
|
+
const response = await fetchGitHub("https://github.com/login/device/code", {
|
|
1315
1343
|
method: "POST",
|
|
1316
1344
|
headers: {
|
|
1317
1345
|
Accept: "application/json",
|
|
1318
1346
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
1319
1347
|
},
|
|
1320
1348
|
body
|
|
1321
|
-
});
|
|
1349
|
+
}, "start browser login");
|
|
1322
1350
|
if (!response.ok) {
|
|
1323
|
-
throw new Error(
|
|
1351
|
+
throw new Error(await getGitHubStatusGuidance(response, "start browser login"));
|
|
1324
1352
|
}
|
|
1325
1353
|
const payload = (await response.json());
|
|
1326
1354
|
if (!payload.device_code || !payload.user_code || !payload.verification_uri) {
|
|
@@ -1484,7 +1512,7 @@ export async function createPullRequest(options = {}) {
|
|
|
1484
1512
|
}
|
|
1485
1513
|
await runGitStrict(workspacePath, ["add", "-A"]);
|
|
1486
1514
|
await runGitStrict(workspacePath, ["commit", "-m", commitMessage]);
|
|
1487
|
-
await
|
|
1515
|
+
await pushPullRequestBranch(workspacePath, branch);
|
|
1488
1516
|
const storedGitHubToken = github.tokenSource === "openpome" ? await readStoredGitHubOAuth() : undefined;
|
|
1489
1517
|
const prProvider = storedGitHubToken?.accessToken ? "github-api" : "github-cli";
|
|
1490
1518
|
const prUrl = storedGitHubToken?.accessToken
|
|
@@ -1875,15 +1903,15 @@ async function isGitHubCliAuthenticated() {
|
|
|
1875
1903
|
}
|
|
1876
1904
|
}
|
|
1877
1905
|
async function fetchGitHubAuthenticatedUser(accessToken) {
|
|
1878
|
-
const response = await
|
|
1906
|
+
const response = await fetchGitHub("https://api.github.com/user", {
|
|
1879
1907
|
headers: {
|
|
1880
1908
|
Accept: "application/vnd.github+json",
|
|
1881
1909
|
Authorization: `Bearer ${accessToken}`,
|
|
1882
1910
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
1883
1911
|
}
|
|
1884
|
-
});
|
|
1912
|
+
}, "verify authenticated user");
|
|
1885
1913
|
if (!response.ok) {
|
|
1886
|
-
throw new Error(
|
|
1914
|
+
throw new Error(await getGitHubStatusGuidance(response, "verify authenticated user"));
|
|
1887
1915
|
}
|
|
1888
1916
|
const payload = (await response.json());
|
|
1889
1917
|
if (!payload.login) {
|
|
@@ -1896,7 +1924,7 @@ async function fetchGitHubAuthenticatedUser(accessToken) {
|
|
|
1896
1924
|
};
|
|
1897
1925
|
}
|
|
1898
1926
|
async function pollGitHubDeviceAccessToken(clientId, deviceCode) {
|
|
1899
|
-
const response = await
|
|
1927
|
+
const response = await fetchGitHub("https://github.com/login/oauth/access_token", {
|
|
1900
1928
|
method: "POST",
|
|
1901
1929
|
headers: {
|
|
1902
1930
|
Accept: "application/json",
|
|
@@ -1907,11 +1935,11 @@ async function pollGitHubDeviceAccessToken(clientId, deviceCode) {
|
|
|
1907
1935
|
device_code: deviceCode,
|
|
1908
1936
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
1909
1937
|
})
|
|
1910
|
-
});
|
|
1938
|
+
}, "complete browser login");
|
|
1911
1939
|
if (!response.ok) {
|
|
1912
1940
|
return {
|
|
1913
1941
|
status: "error",
|
|
1914
|
-
detail:
|
|
1942
|
+
detail: await getGitHubStatusGuidance(response, "complete browser login")
|
|
1915
1943
|
};
|
|
1916
1944
|
}
|
|
1917
1945
|
const payload = (await response.json());
|
|
@@ -1949,6 +1977,48 @@ function parseGitHubScopes(scope) {
|
|
|
1949
1977
|
.map((value) => value.trim())
|
|
1950
1978
|
.filter((value) => value.length > 0);
|
|
1951
1979
|
}
|
|
1980
|
+
async function fetchGitHub(input, init, action) {
|
|
1981
|
+
try {
|
|
1982
|
+
return await fetch(input, init);
|
|
1983
|
+
}
|
|
1984
|
+
catch (error) {
|
|
1985
|
+
throw new Error(getGitHubNetworkGuidance(action, error));
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
async function getGitHubStatusGuidance(response, action) {
|
|
1989
|
+
const body = await safeResponseText(response);
|
|
1990
|
+
const detail = body ? ` Detail: ${summarizeProviderBody(body)}` : "";
|
|
1991
|
+
if (response.status === 401) {
|
|
1992
|
+
return `GitHub ${action} was unauthorized (401). Run \`pome auth github login\` again, or \`gh auth login\` if you use the GitHub CLI fallback.${detail}`;
|
|
1993
|
+
}
|
|
1994
|
+
if (response.status === 403) {
|
|
1995
|
+
return `GitHub ${action} was forbidden (403). Check repository permission, organization SSO, token scopes, branch protection, and whether the token has \`repo\` access.${detail}`;
|
|
1996
|
+
}
|
|
1997
|
+
if (response.status === 404) {
|
|
1998
|
+
return `GitHub ${action} could not find the repository or resource (404). Check the git remote, repository visibility, GitHub Enterprise host, and account access.${detail}`;
|
|
1999
|
+
}
|
|
2000
|
+
if (response.status === 422) {
|
|
2001
|
+
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}`;
|
|
2002
|
+
}
|
|
2003
|
+
if (response.status === 429 || response.headers.get("x-ratelimit-remaining") === "0") {
|
|
2004
|
+
const reset = response.headers.get("x-ratelimit-reset");
|
|
2005
|
+
const resetDetail = reset ? ` Rate limit resets at ${new Date(Number(reset) * 1000).toISOString()}.` : "";
|
|
2006
|
+
return `GitHub rate limit reached while trying to ${action}.${resetDetail} Wait and retry, or use a token with the right organization access.${detail}`;
|
|
2007
|
+
}
|
|
2008
|
+
if (response.status >= 500) {
|
|
2009
|
+
return `GitHub ${action} failed with ${response.status} ${response.statusText}. GitHub may be unavailable, blocked by a proxy, or unreachable from this network.${detail}`;
|
|
2010
|
+
}
|
|
2011
|
+
return `GitHub ${action} failed: ${response.status} ${response.statusText}.${detail}`;
|
|
2012
|
+
}
|
|
2013
|
+
function getGitHubNetworkGuidance(action, error) {
|
|
2014
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2015
|
+
return [
|
|
2016
|
+
`GitHub ${action} could not reach GitHub.`,
|
|
2017
|
+
"Check internet access, VPN split tunneling, proxy/firewall rules, corporate certificate trust, and GitHub Enterprise host configuration.",
|
|
2018
|
+
"Run `pome auth github status` after fixing network access.",
|
|
2019
|
+
`Detail: ${detail}`
|
|
2020
|
+
].join(" ");
|
|
2021
|
+
}
|
|
1952
2022
|
function summarizeUnknownError(error) {
|
|
1953
2023
|
return error instanceof Error ? error.message : String(error);
|
|
1954
2024
|
}
|
|
@@ -1997,7 +2067,11 @@ function getOpenPomePaths() {
|
|
|
1997
2067
|
async function readConfigIfPresent(configFile) {
|
|
1998
2068
|
try {
|
|
1999
2069
|
const content = await readFile(configFile, "utf8");
|
|
2000
|
-
return
|
|
2070
|
+
return {
|
|
2071
|
+
...defaultConfig,
|
|
2072
|
+
...JSON.parse(content),
|
|
2073
|
+
telemetryEnabled: false
|
|
2074
|
+
};
|
|
2001
2075
|
}
|
|
2002
2076
|
catch (error) {
|
|
2003
2077
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -2544,6 +2618,7 @@ async function readActiveTaskSessionIfPresent(homeDirectory) {
|
|
|
2544
2618
|
async function writeActiveTaskSession(homeDirectory, session) {
|
|
2545
2619
|
await mkdir(homeDirectory, { recursive: true });
|
|
2546
2620
|
await writeFile(getActiveTaskSessionFile(homeDirectory), `${JSON.stringify(session, null, 2)}\n`, "utf8");
|
|
2621
|
+
persistTaskSessionSnapshot(homeDirectory, session, true);
|
|
2547
2622
|
}
|
|
2548
2623
|
async function removeActiveTaskSession(homeDirectory) {
|
|
2549
2624
|
await rm(getActiveTaskSessionFile(homeDirectory), { force: true });
|
|
@@ -2571,6 +2646,88 @@ async function archiveTaskSession(homeDirectory, session) {
|
|
|
2571
2646
|
};
|
|
2572
2647
|
await mkdir(homeDirectory, { recursive: true });
|
|
2573
2648
|
await writeFile(getTaskSessionHistoryFile(homeDirectory), `${JSON.stringify(history, null, 2)}\n`, "utf8");
|
|
2649
|
+
persistTaskSessionSnapshot(homeDirectory, session, false);
|
|
2650
|
+
}
|
|
2651
|
+
async function listArchivedTaskSessions(homeDirectory, jsonSessions, limit = 25) {
|
|
2652
|
+
const sqliteSessions = tryListSessionSnapshots(homeDirectory, limit);
|
|
2653
|
+
if (sqliteSessions.length > 0) {
|
|
2654
|
+
return sqliteSessions;
|
|
2655
|
+
}
|
|
2656
|
+
return jsonSessions.slice(0, limit);
|
|
2657
|
+
}
|
|
2658
|
+
function persistTaskSessionSnapshot(homeDirectory, session, active) {
|
|
2659
|
+
try {
|
|
2660
|
+
const store = openSessionSnapshotStore(homeDirectory);
|
|
2661
|
+
store.upsertSessionSnapshot(buildSessionSnapshotInput(session, active));
|
|
2662
|
+
}
|
|
2663
|
+
catch {
|
|
2664
|
+
// JSON active/history files remain the alpha compatibility fallback. SQLite
|
|
2665
|
+
// persistence must improve resume reliability without blocking daily work.
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
function tryListSessionSnapshots(homeDirectory, limit) {
|
|
2669
|
+
try {
|
|
2670
|
+
const store = openSessionSnapshotStore(homeDirectory);
|
|
2671
|
+
return store.listSessionSnapshots(limit)
|
|
2672
|
+
.map((record) => record.snapshot)
|
|
2673
|
+
.filter(isPersistedTaskSession);
|
|
2674
|
+
}
|
|
2675
|
+
catch {
|
|
2676
|
+
return [];
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
function buildSessionSnapshotInput(session, active) {
|
|
2680
|
+
const latestEvent = session.events?.[session.events.length - 1];
|
|
2681
|
+
const latestTestRun = session.testRunEvidence?.[session.testRunEvidence.length - 1];
|
|
2682
|
+
return {
|
|
2683
|
+
sessionId: session.session.id,
|
|
2684
|
+
workItemKey: session.workItem.key,
|
|
2685
|
+
workItemTitle: session.workItem.title,
|
|
2686
|
+
status: session.session.status,
|
|
2687
|
+
updatedAt: session.session.updatedAt,
|
|
2688
|
+
active,
|
|
2689
|
+
workspaceName: session.workspaceCandidate?.workspace.name,
|
|
2690
|
+
workspacePath: session.workspaceCandidate?.workspace.path,
|
|
2691
|
+
latestEventTitle: latestEvent?.title,
|
|
2692
|
+
latestEventAt: latestEvent?.createdAt,
|
|
2693
|
+
latestTestStatus: latestTestRun?.status,
|
|
2694
|
+
latestTestCommand: latestTestRun?.command,
|
|
2695
|
+
latestPatchAppliedAt: session.aiPatchProposal?.appliedAt,
|
|
2696
|
+
prUrl: session.prCreation?.prUrl,
|
|
2697
|
+
jiraCommentId: session.workItemUpdatePost?.commentId,
|
|
2698
|
+
snapshot: session
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
function summarizeTaskSessionHistory(session) {
|
|
2702
|
+
const latestEvent = session.events?.[session.events.length - 1];
|
|
2703
|
+
const latestTestRun = session.testRunEvidence?.[session.testRunEvidence.length - 1];
|
|
2704
|
+
return {
|
|
2705
|
+
sessionId: session.session.id,
|
|
2706
|
+
active: session.session.status !== "completed" && session.session.status !== "blocked",
|
|
2707
|
+
workItemKey: session.workItem.key,
|
|
2708
|
+
workItemTitle: session.workItem.title,
|
|
2709
|
+
status: session.session.status,
|
|
2710
|
+
updatedAt: session.session.updatedAt,
|
|
2711
|
+
workspaceName: session.workspaceCandidate?.workspace.name,
|
|
2712
|
+
workspacePath: session.workspaceCandidate?.workspace.path,
|
|
2713
|
+
latestEventTitle: latestEvent?.title,
|
|
2714
|
+
latestEventAt: latestEvent?.createdAt,
|
|
2715
|
+
latestTestStatus: latestTestRun?.status,
|
|
2716
|
+
latestTestCommand: latestTestRun?.command,
|
|
2717
|
+
latestPatchAppliedAt: session.aiPatchProposal?.appliedAt,
|
|
2718
|
+
prUrl: session.prCreation?.prUrl,
|
|
2719
|
+
jiraCommentId: session.workItemUpdatePost?.commentId
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
function isPersistedTaskSession(value) {
|
|
2723
|
+
return typeof value === "object"
|
|
2724
|
+
&& value !== null
|
|
2725
|
+
&& "version" in value
|
|
2726
|
+
&& "session" in value
|
|
2727
|
+
&& "workItem" in value;
|
|
2728
|
+
}
|
|
2729
|
+
function getSessionDatabaseFile(homeDirectory) {
|
|
2730
|
+
return getLocalPersistenceInfo(homeDirectory).databaseFile;
|
|
2574
2731
|
}
|
|
2575
2732
|
function selectArchivedTaskSession(sessions, sessionId) {
|
|
2576
2733
|
if (sessionId) {
|
|
@@ -2580,28 +2737,39 @@ function selectArchivedTaskSession(sessions, sessionId) {
|
|
|
2580
2737
|
}
|
|
2581
2738
|
function buildPlanningContext(session) {
|
|
2582
2739
|
const workspace = session.workspaceCandidate?.workspace;
|
|
2740
|
+
const missingRequirementSignals = detectMissingRequirementSignals(session.workItem);
|
|
2583
2741
|
const context = [
|
|
2584
2742
|
`Work item type: ${session.workItem.type}`,
|
|
2585
2743
|
`Status: ${session.workItem.status}`,
|
|
2586
2744
|
session.workItem.priority ? `Priority: ${session.workItem.priority}` : undefined,
|
|
2745
|
+
session.workItem.description ? `Description length: ${session.workItem.description.length} characters` : "Description: not provided",
|
|
2587
2746
|
hasExplicitAcceptanceCriteria(session.workItem)
|
|
2588
2747
|
? "Acceptance criteria: detected in work item text"
|
|
2589
2748
|
: "Acceptance criteria: not explicit; identify missing acceptance criteria before implementation",
|
|
2749
|
+
missingRequirementSignals.length ? `Missing requirement signals: ${missingRequirementSignals.join("; ")}` : undefined,
|
|
2590
2750
|
session.workItem.labels?.length ? `Labels: ${session.workItem.labels.join(", ")}` : undefined,
|
|
2591
2751
|
session.workItem.components?.length ? `Components: ${session.workItem.components.join(", ")}` : undefined,
|
|
2752
|
+
session.workItem.links?.length ? `Linked references: ${session.workItem.links.map((link) => `${link.kind}:${link.title ?? link.url}`).join("; ")}` : undefined,
|
|
2753
|
+
session.workItem.subtasks?.length ? `Subtasks: ${session.workItem.subtasks.map((subtask) => `${subtask.key} ${subtask.status} ${subtask.title}`).join("; ")}` : undefined,
|
|
2592
2754
|
workspace ? `Workspace: ${workspace.name}` : "Workspace: unresolved",
|
|
2593
2755
|
workspace?.path ? `Workspace path: ${workspace.path}` : undefined,
|
|
2594
2756
|
session.workspaceCandidate ? `Workspace confidence: ${Math.round(session.workspaceCandidate.confidence * 100)}%` : undefined,
|
|
2595
|
-
session.workspaceCandidate?.reasons.length ? `Workspace reasons: ${session.workspaceCandidate.reasons.join("; ")}` : undefined
|
|
2757
|
+
session.workspaceCandidate?.reasons.length ? `Workspace reasons: ${session.workspaceCandidate.reasons.join("; ")}` : undefined,
|
|
2758
|
+
workspace?.packageNames?.length ? `Workspace packages: ${workspace.packageNames.slice(0, 8).join(", ")}` : undefined,
|
|
2759
|
+
workspace?.readmeKeywords?.length ? `README signals: ${workspace.readmeKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
2760
|
+
workspace?.codeownersKeywords?.length ? `Ownership signals: ${workspace.codeownersKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
2761
|
+
workspace?.recentBranches?.length ? `Recent branches: ${workspace.recentBranches.slice(0, 8).join(", ")}` : undefined,
|
|
2762
|
+
workspace?.recentCommitRefs?.length ? `Recent work refs: ${workspace.recentCommitRefs.slice(0, 12).join(", ")}` : undefined
|
|
2596
2763
|
];
|
|
2597
2764
|
return context.filter((item) => Boolean(item));
|
|
2598
2765
|
}
|
|
2599
2766
|
function buildInitialImplementationPlan(workItem, workspaceCandidate) {
|
|
2600
2767
|
const workspace = workspaceCandidate?.workspace;
|
|
2601
2768
|
const hasWorkspace = Boolean(workspace?.path);
|
|
2769
|
+
const missingRequirementSignals = detectMissingRequirementSignals(workItem);
|
|
2602
2770
|
const missingInfo = [
|
|
2603
2771
|
hasWorkspace ? undefined : "No workspace candidate is selected yet.",
|
|
2604
|
-
|
|
2772
|
+
...missingRequirementSignals
|
|
2605
2773
|
].filter((item) => Boolean(item));
|
|
2606
2774
|
return {
|
|
2607
2775
|
summary: `Prepare implementation for ${workItem.key}: ${workItem.title}`,
|
|
@@ -2647,7 +2815,36 @@ function buildInitialImplementationPlan(workItem, workspaceCandidate) {
|
|
|
2647
2815
|
}
|
|
2648
2816
|
function hasExplicitAcceptanceCriteria(workItem) {
|
|
2649
2817
|
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);
|
|
2818
|
+
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);
|
|
2819
|
+
}
|
|
2820
|
+
function detectMissingRequirementSignals(workItem) {
|
|
2821
|
+
const text = [workItem.title, workItem.description].filter(Boolean).join("\n").trim();
|
|
2822
|
+
const lower = text.toLowerCase();
|
|
2823
|
+
const signals = [];
|
|
2824
|
+
if (!workItem.description || workItem.description.trim().length < 40) {
|
|
2825
|
+
signals.push("Clarify the exact scope because the work item description is short.");
|
|
2826
|
+
}
|
|
2827
|
+
if (!hasExplicitAcceptanceCriteria(workItem)) {
|
|
2828
|
+
signals.push("Ask what acceptance criteria prove this story is complete.");
|
|
2829
|
+
}
|
|
2830
|
+
if (workItem.type === "bug") {
|
|
2831
|
+
const hasExpected = /\b(expected|should happen|desired behavior|correct behavior)\b/u.test(lower);
|
|
2832
|
+
const hasActual = /\b(actual|currently|observed|happens now|error|failure|failed)\b/u.test(lower);
|
|
2833
|
+
const hasRepro = /\b(steps to reproduce|repro|reproduce|given\b.*\bwhen\b.*\bthen)\b/su.test(lower);
|
|
2834
|
+
if (!hasExpected || !hasActual) {
|
|
2835
|
+
signals.push("Ask for clear expected behavior and actual behavior for this bug.");
|
|
2836
|
+
}
|
|
2837
|
+
if (!hasRepro) {
|
|
2838
|
+
signals.push("Ask for reproduction steps or a failing scenario for this bug.");
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
if (!workItem.labels?.length && !workItem.components?.length) {
|
|
2842
|
+
signals.push("Ask which component, service, or package owns this change.");
|
|
2843
|
+
}
|
|
2844
|
+
if (!workItem.links?.some((link) => link.kind === "code" || link.kind === "pull_request" || link.kind === "document")) {
|
|
2845
|
+
signals.push("Ask whether there is linked code, a prior pull request, design doc, or reference issue.");
|
|
2846
|
+
}
|
|
2847
|
+
return Array.from(new Set(signals)).slice(0, 6);
|
|
2651
2848
|
}
|
|
2652
2849
|
async function buildImplementationPlan(persisted, prompt) {
|
|
2653
2850
|
const config = await readConfigIfPresent(getOpenPomePaths().configFile);
|
|
@@ -2674,7 +2871,7 @@ async function completeModelText(provider, prompt) {
|
|
|
2674
2871
|
return completeClaudeCliText(prompt);
|
|
2675
2872
|
}
|
|
2676
2873
|
async function completeOpenAIText(prompt, apiKey) {
|
|
2677
|
-
const response = await
|
|
2874
|
+
const response = await fetchModelProvider("OpenAI", "https://api.openai.com/v1/responses", {
|
|
2678
2875
|
method: "POST",
|
|
2679
2876
|
headers: {
|
|
2680
2877
|
"authorization": `Bearer ${apiKey}`,
|
|
@@ -2686,7 +2883,7 @@ async function completeOpenAIText(prompt, apiKey) {
|
|
|
2686
2883
|
})
|
|
2687
2884
|
});
|
|
2688
2885
|
if (!response.ok) {
|
|
2689
|
-
throw new Error(
|
|
2886
|
+
throw new Error(await getModelProviderStatusGuidance("OpenAI", response, "generate a plan or patch"));
|
|
2690
2887
|
}
|
|
2691
2888
|
const body = await response.json();
|
|
2692
2889
|
if (typeof body.output_text === "string") {
|
|
@@ -2700,7 +2897,7 @@ async function completeOpenAIText(prompt, apiKey) {
|
|
|
2700
2897
|
.join("\n");
|
|
2701
2898
|
}
|
|
2702
2899
|
async function completeAnthropicText(prompt, apiKey) {
|
|
2703
|
-
const response = await
|
|
2900
|
+
const response = await fetchModelProvider("Claude", "https://api.anthropic.com/v1/messages", {
|
|
2704
2901
|
method: "POST",
|
|
2705
2902
|
headers: {
|
|
2706
2903
|
"x-api-key": apiKey,
|
|
@@ -2719,7 +2916,7 @@ async function completeAnthropicText(prompt, apiKey) {
|
|
|
2719
2916
|
})
|
|
2720
2917
|
});
|
|
2721
2918
|
if (!response.ok) {
|
|
2722
|
-
throw new Error(
|
|
2919
|
+
throw new Error(await getModelProviderStatusGuidance("Claude", response, "generate a plan or patch"));
|
|
2723
2920
|
}
|
|
2724
2921
|
const body = await response.json();
|
|
2725
2922
|
const content = Array.isArray(body.content) ? body.content : [];
|
|
@@ -2761,12 +2958,50 @@ async function completeClaudeCliText(prompt) {
|
|
|
2761
2958
|
throw new Error(`Claude CLI request failed: ${summarizeExecError(error) || String(error)}`);
|
|
2762
2959
|
}
|
|
2763
2960
|
}
|
|
2961
|
+
async function fetchModelProvider(provider, input, init) {
|
|
2962
|
+
try {
|
|
2963
|
+
return await fetch(input, init);
|
|
2964
|
+
}
|
|
2965
|
+
catch (error) {
|
|
2966
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2967
|
+
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}`);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
async function getModelProviderStatusGuidance(provider, response, action) {
|
|
2971
|
+
const body = await safeResponseText(response);
|
|
2972
|
+
const detail = body ? ` Detail: ${summarizeProviderBody(body)}` : "";
|
|
2973
|
+
const authCommand = provider === "OpenAI" ? "pome auth ai openai" : "pome auth ai claude";
|
|
2974
|
+
if (response.status === 401) {
|
|
2975
|
+
return `${provider} ${action} was unauthorized (401). Reconnect with \`${authCommand}\` or verify the provider API key in your OS credential store/environment.${detail}`;
|
|
2976
|
+
}
|
|
2977
|
+
if (response.status === 403) {
|
|
2978
|
+
return `${provider} ${action} was forbidden (403). Check organization policy, model access, provider project permissions, and corporate egress rules.${detail}`;
|
|
2979
|
+
}
|
|
2980
|
+
if (response.status === 404) {
|
|
2981
|
+
return `${provider} ${action} could not find the configured model or endpoint (404). Check OPENPOME_${provider === "OpenAI" ? "OPENAI_MODEL" : "ANTHROPIC_MODEL"} and provider access.${detail}`;
|
|
2982
|
+
}
|
|
2983
|
+
if (response.status === 408 || response.status === 409 || response.status === 429) {
|
|
2984
|
+
return `${provider} is busy or rate limited (${response.status}). Wait and retry, or choose a smaller model/context. OpenPome has not written files.${detail}`;
|
|
2985
|
+
}
|
|
2986
|
+
if (response.status >= 500) {
|
|
2987
|
+
return `${provider} failed with ${response.status} ${response.statusText}. Provider service may be unavailable or blocked by your network/proxy. Retry later.${detail}`;
|
|
2988
|
+
}
|
|
2989
|
+
return `${provider} ${action} failed: ${response.status} ${response.statusText}.${detail}`;
|
|
2990
|
+
}
|
|
2764
2991
|
function buildStructuredPlanPrompt(prompt) {
|
|
2765
2992
|
return [
|
|
2766
2993
|
"You are OpenPome's planning engine.",
|
|
2994
|
+
"Plan like a senior developer assistant working from a live corporate work item.",
|
|
2995
|
+
"Prefer the smallest repo-aware change that satisfies the work item. Call out unclear scope instead of inventing requirements.",
|
|
2996
|
+
"Use workspace metadata, labels, linked references, ownership signals, and recent branch/commit refs to rank likely files.",
|
|
2997
|
+
"Suggest targeted validation commands before broad commands when the work item points to a specific component.",
|
|
2767
2998
|
"Return only compact JSON with this exact shape:",
|
|
2768
2999
|
"{\"summary\":\"...\",\"assumptions\":[\"...\"],\"steps\":[{\"id\":\"1\",\"title\":\"...\",\"detail\":\"...\"}],\"filesLikelyChanged\":[\"...\"],\"commandsToRun\":[\"...\"],\"risks\":[\"...\"],\"missingInfo\":[\"...\"]}",
|
|
2769
|
-
"
|
|
3000
|
+
"Rules:",
|
|
3001
|
+
"- Do not include source code, full diffs, secrets, or markdown fences.",
|
|
3002
|
+
"- Put missing acceptance criteria, missing repro steps, unclear expected behavior, and missing code links in missingInfo.",
|
|
3003
|
+
"- Keep filesLikelyChanged to relative paths or package/module hints when exact files are unknown.",
|
|
3004
|
+
"- Keep commandsToRun executable from the selected workspace.",
|
|
2770
3005
|
"",
|
|
2771
3006
|
prompt
|
|
2772
3007
|
].join("\n");
|
|
@@ -2825,33 +3060,65 @@ const modelProviderTimeoutMs = 120_000;
|
|
|
2825
3060
|
const modelProviderMaxBufferBytes = 2 * 1024 * 1024;
|
|
2826
3061
|
const sensitivePathFragments = [
|
|
2827
3062
|
".env",
|
|
3063
|
+
".env.local",
|
|
3064
|
+
".env.production",
|
|
3065
|
+
".env.development",
|
|
2828
3066
|
".npmrc",
|
|
3067
|
+
".yarnrc",
|
|
3068
|
+
".pnpmrc",
|
|
2829
3069
|
".pypirc",
|
|
2830
3070
|
".netrc",
|
|
2831
3071
|
".ssh",
|
|
2832
3072
|
".aws",
|
|
2833
3073
|
".gcp",
|
|
2834
3074
|
".azure",
|
|
3075
|
+
".kube",
|
|
3076
|
+
".docker",
|
|
3077
|
+
"credentials",
|
|
3078
|
+
"credential",
|
|
3079
|
+
"secrets",
|
|
3080
|
+
"secret",
|
|
3081
|
+
"private-key",
|
|
3082
|
+
"private_key",
|
|
2835
3083
|
"id_rsa",
|
|
2836
3084
|
"id_dsa",
|
|
2837
|
-
"id_ed25519"
|
|
3085
|
+
"id_ed25519",
|
|
3086
|
+
".pem",
|
|
3087
|
+
".key",
|
|
3088
|
+
".crt",
|
|
3089
|
+
".p12",
|
|
3090
|
+
".pfx"
|
|
3091
|
+
];
|
|
3092
|
+
const sensitiveContentPatterns = [
|
|
3093
|
+
/-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/u,
|
|
3094
|
+
/\b(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|OPENPOME_JIRA_API_TOKEN|OPENPOME_GITHUB_TOKEN|GITHUB_TOKEN|NPM_TOKEN)\s*=\s*['"]?[^'"\s]+/iu,
|
|
3095
|
+
/\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|private[_-]?key)\s*[:=]\s*['"][^'"]{12,}['"]/iu,
|
|
3096
|
+
/\b(?:ghp|github_pat|npm|sk|sk-ant|xox[baprs])-?[A-Za-z0-9_=-]{20,}\b/u
|
|
2838
3097
|
];
|
|
2839
3098
|
async function collectPatchContextFiles(workspacePath, session) {
|
|
2840
3099
|
const candidates = [];
|
|
2841
3100
|
for (const filePath of session.plan?.filesLikelyChanged ?? []) {
|
|
2842
3101
|
const normalized = normalizeWorkspaceRelativePath(workspacePath, filePath, "skip");
|
|
2843
3102
|
if (normalized && normalized !== ".") {
|
|
2844
|
-
candidates.push(
|
|
3103
|
+
candidates.push({
|
|
3104
|
+
filePath: normalized,
|
|
3105
|
+
score: 80,
|
|
3106
|
+
reason: "AI plan marked this file as likely impacted."
|
|
3107
|
+
});
|
|
2845
3108
|
}
|
|
2846
3109
|
}
|
|
2847
|
-
candidates.push("package.json", "README.md", "AGENTS.md");
|
|
3110
|
+
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
3111
|
const trackedFiles = await listTrackedWorkspaceFiles(workspacePath);
|
|
2849
3112
|
const tokens = tokenizePatchSearchText([
|
|
2850
3113
|
session.workItem.key,
|
|
2851
3114
|
session.workItem.title,
|
|
2852
3115
|
session.workItem.description,
|
|
3116
|
+
session.plan?.summary,
|
|
3117
|
+
...(session.plan?.steps.map((step) => `${step.title} ${step.detail ?? ""}`) ?? []),
|
|
2853
3118
|
...(session.workItem.labels ?? []),
|
|
2854
|
-
...(session.workItem.components ?? [])
|
|
3119
|
+
...(session.workItem.components ?? []),
|
|
3120
|
+
...(session.workspaceCandidate?.workspace.packageNames ?? []),
|
|
3121
|
+
...(session.workspaceCandidate?.workspace.readmeKeywords ?? [])
|
|
2855
3122
|
].filter((value) => Boolean(value)).join(" "));
|
|
2856
3123
|
const planHints = new Set((session.plan?.filesLikelyChanged ?? [])
|
|
2857
3124
|
.map((filePath) => normalizeWorkspaceRelativePath(workspacePath, filePath, "skip"))
|
|
@@ -2859,12 +3126,13 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2859
3126
|
const rankedTrackedFiles = trackedFiles
|
|
2860
3127
|
.map((filePath) => ({
|
|
2861
3128
|
filePath,
|
|
2862
|
-
score: scorePatchContextFile(filePath, tokens, planHints)
|
|
3129
|
+
score: scorePatchContextFile(filePath, tokens, planHints),
|
|
3130
|
+
reason: describePatchContextReason(filePath, tokens, planHints)
|
|
2863
3131
|
}))
|
|
2864
3132
|
.filter((candidate) => candidate.score > 0)
|
|
2865
3133
|
.sort((left, right) => right.score - left.score || left.filePath.localeCompare(right.filePath));
|
|
2866
3134
|
for (const candidate of rankedTrackedFiles.slice(0, 40)) {
|
|
2867
|
-
candidates.push(candidate
|
|
3135
|
+
candidates.push(candidate);
|
|
2868
3136
|
}
|
|
2869
3137
|
const selected = [];
|
|
2870
3138
|
const seen = new Set();
|
|
@@ -2873,7 +3141,7 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2873
3141
|
if (selected.length >= maxPatchContextFiles || totalBytes >= maxPatchContextTotalBytes) {
|
|
2874
3142
|
break;
|
|
2875
3143
|
}
|
|
2876
|
-
const relativePath = normalizeWorkspaceRelativePath(workspacePath, candidate, "skip");
|
|
3144
|
+
const relativePath = normalizeWorkspaceRelativePath(workspacePath, candidate.filePath, "skip");
|
|
2877
3145
|
if (!relativePath || seen.has(relativePath) || isSensitiveWorkspacePath(relativePath)) {
|
|
2878
3146
|
continue;
|
|
2879
3147
|
}
|
|
@@ -2881,7 +3149,7 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2881
3149
|
const absolutePath = resolve(workspacePath, relativePath);
|
|
2882
3150
|
try {
|
|
2883
3151
|
const content = await readFile(absolutePath, "utf8");
|
|
2884
|
-
if (content.includes("\u0000")) {
|
|
3152
|
+
if (content.includes("\u0000") || containsSensitiveContent(content)) {
|
|
2885
3153
|
continue;
|
|
2886
3154
|
}
|
|
2887
3155
|
const remainingBytes = maxPatchContextTotalBytes - totalBytes;
|
|
@@ -2891,7 +3159,9 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2891
3159
|
selected.push({
|
|
2892
3160
|
path: relativePath,
|
|
2893
3161
|
content: sliced,
|
|
2894
|
-
truncated: Buffer.byteLength(content, "utf8") > Buffer.byteLength(sliced, "utf8")
|
|
3162
|
+
truncated: Buffer.byteLength(content, "utf8") > Buffer.byteLength(sliced, "utf8"),
|
|
3163
|
+
score: candidate.score,
|
|
3164
|
+
reason: candidate.reason
|
|
2895
3165
|
});
|
|
2896
3166
|
}
|
|
2897
3167
|
catch {
|
|
@@ -2902,12 +3172,45 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2902
3172
|
}
|
|
2903
3173
|
async function listTrackedWorkspaceFiles(workspacePath) {
|
|
2904
3174
|
const output = await runGit(workspacePath, ["ls-files"]);
|
|
2905
|
-
|
|
3175
|
+
const trackedFiles = output
|
|
2906
3176
|
.split(/\r?\n/u)
|
|
2907
3177
|
.map((line) => line.trim())
|
|
2908
3178
|
.filter(Boolean)
|
|
2909
3179
|
.filter((filePath) => !isSensitiveWorkspacePath(filePath))
|
|
2910
3180
|
.slice(0, 1000);
|
|
3181
|
+
return trackedFiles.length > 0 ? trackedFiles : listWorkspaceFilesFallback(workspacePath);
|
|
3182
|
+
}
|
|
3183
|
+
async function listWorkspaceFilesFallback(workspacePath) {
|
|
3184
|
+
const collected = [];
|
|
3185
|
+
const queue = ["."];
|
|
3186
|
+
const ignoredDirectories = new Set([".git", "node_modules", "dist", "build", "coverage", ".next", ".turbo", ".cache", "vendor"]);
|
|
3187
|
+
while (queue.length > 0 && collected.length < 1000) {
|
|
3188
|
+
const current = queue.shift() ?? ".";
|
|
3189
|
+
const absoluteCurrent = resolve(workspacePath, current);
|
|
3190
|
+
let directory;
|
|
3191
|
+
try {
|
|
3192
|
+
directory = await opendir(absoluteCurrent);
|
|
3193
|
+
}
|
|
3194
|
+
catch {
|
|
3195
|
+
continue;
|
|
3196
|
+
}
|
|
3197
|
+
for await (const entry of directory) {
|
|
3198
|
+
const relativePath = current === "." ? entry.name : `${current}/${entry.name}`;
|
|
3199
|
+
if (entry.isDirectory()) {
|
|
3200
|
+
if (!ignoredDirectories.has(entry.name) && !isSensitiveWorkspacePath(relativePath)) {
|
|
3201
|
+
queue.push(relativePath);
|
|
3202
|
+
}
|
|
3203
|
+
continue;
|
|
3204
|
+
}
|
|
3205
|
+
if (entry.isFile() && !isSensitiveWorkspacePath(relativePath)) {
|
|
3206
|
+
collected.push(relativePath);
|
|
3207
|
+
if (collected.length >= 1000) {
|
|
3208
|
+
break;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
return collected;
|
|
2911
3214
|
}
|
|
2912
3215
|
function tokenizePatchSearchText(value) {
|
|
2913
3216
|
return Array.from(new Set(value.toLowerCase().split(/[^a-z0-9]+/u).filter((token) => token.length >= 3))).slice(0, 20);
|
|
@@ -2916,7 +3219,7 @@ function scorePatchContextFile(filePath, tokens, planHints) {
|
|
|
2916
3219
|
const lower = filePath.toLowerCase();
|
|
2917
3220
|
let score = 0;
|
|
2918
3221
|
if (planHints.has(filePath)) {
|
|
2919
|
-
score +=
|
|
3222
|
+
score += 40;
|
|
2920
3223
|
}
|
|
2921
3224
|
for (const token of tokens) {
|
|
2922
3225
|
if (lower.includes(token)) {
|
|
@@ -2926,32 +3229,64 @@ function scorePatchContextFile(filePath, tokens, planHints) {
|
|
|
2926
3229
|
if (/\.(ts|tsx|js|jsx|mjs|cjs|py|java|kt|go|rs|rb|php|cs|swift)$/u.test(lower)) {
|
|
2927
3230
|
score += 4;
|
|
2928
3231
|
}
|
|
3232
|
+
if (/(src|app|lib|packages|services|connectors|components|routes|api)\//u.test(lower)) {
|
|
3233
|
+
score += 4;
|
|
3234
|
+
}
|
|
2929
3235
|
if (/(test|spec|__tests__|tests)\b/u.test(lower)) {
|
|
2930
|
-
score +=
|
|
3236
|
+
score += 5;
|
|
2931
3237
|
}
|
|
2932
3238
|
if (/(readme|package\.json|codeowners|agents\.md|tsconfig|vite|jest|vitest|pytest|gradle|pom\.xml|go\.mod|cargo\.toml)/u.test(lower)) {
|
|
2933
3239
|
score += 2;
|
|
2934
3240
|
}
|
|
2935
|
-
if (/(dist|build|coverage|node_modules|vendor|generated|\.lock$|lockfile)/u.test(lower)) {
|
|
2936
|
-
score -=
|
|
3241
|
+
if (/(dist|build|coverage|node_modules|vendor|generated|\.lock$|lockfile|\.min\.)/u.test(lower)) {
|
|
3242
|
+
score -= 16;
|
|
3243
|
+
}
|
|
3244
|
+
if (/(snapshot|snapshots|fixtures|fixture|mock|mocks)\//u.test(lower)) {
|
|
3245
|
+
score -= 2;
|
|
2937
3246
|
}
|
|
2938
3247
|
return score;
|
|
2939
3248
|
}
|
|
3249
|
+
function describePatchContextReason(filePath, tokens, planHints) {
|
|
3250
|
+
const lower = filePath.toLowerCase();
|
|
3251
|
+
const reasons = [
|
|
3252
|
+
planHints.has(filePath) ? "named by the approved plan" : undefined,
|
|
3253
|
+
tokens.filter((token) => lower.includes(token)).slice(0, 4).length
|
|
3254
|
+
? `matches task token(s): ${tokens.filter((token) => lower.includes(token)).slice(0, 4).join(", ")}`
|
|
3255
|
+
: undefined,
|
|
3256
|
+
/(test|spec|__tests__|tests)\b/u.test(lower) ? "is a related validation file" : undefined,
|
|
3257
|
+
/(package\.json|tsconfig|vite|jest|vitest|pytest|gradle|pom\.xml|go\.mod|cargo\.toml)/u.test(lower) ? "contains project or test configuration" : undefined,
|
|
3258
|
+
/(readme|codeowners|agents\.md)/u.test(lower) ? "contains repository guidance" : undefined,
|
|
3259
|
+
/\.(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
|
|
3260
|
+
].filter((reason) => Boolean(reason));
|
|
3261
|
+
return reasons.length ? reasons.join("; ") : "ranked from repository metadata and work item text";
|
|
3262
|
+
}
|
|
2940
3263
|
function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
2941
3264
|
const plan = session.plan;
|
|
2942
3265
|
const context = contextFiles.map((file) => [
|
|
2943
3266
|
`FILE: ${file.path}${file.truncated ? " (truncated)" : ""}`,
|
|
3267
|
+
`RANK: ${file.score}`,
|
|
3268
|
+
`WHY_INCLUDED: ${file.reason}`,
|
|
2944
3269
|
"```",
|
|
2945
3270
|
file.content,
|
|
2946
3271
|
"```"
|
|
2947
3272
|
].join("\n")).join("\n\n");
|
|
2948
3273
|
const failedTestContext = getFailedTestContextAfterLatestPatch(session);
|
|
3274
|
+
const missingRequirementSignals = Array.from(new Set([
|
|
3275
|
+
...detectMissingRequirementSignals(session.workItem),
|
|
3276
|
+
...(plan?.missingInfo ?? [])
|
|
3277
|
+
])).slice(0, 8);
|
|
3278
|
+
const workspace = session.workspaceCandidate?.workspace;
|
|
2949
3279
|
return [
|
|
2950
3280
|
"You are OpenPome's implementation engine.",
|
|
3281
|
+
failedTestContext.length
|
|
3282
|
+
? "This is a retry after approved validation failed. Repair only the failure using the evidence below."
|
|
3283
|
+
: "This is the first implementation patch for the approved plan.",
|
|
2951
3284
|
"Return only compact JSON. Do not include markdown fences outside JSON.",
|
|
2952
3285
|
"Only propose a minimal safe file patch for the approved work item.",
|
|
2953
3286
|
"Do not include secrets, credentials, generated dependency folders, lockfile rewrites, or unrelated refactors.",
|
|
2954
3287
|
"Use full replacement file content for each proposed file.",
|
|
3288
|
+
"If requirements are unclear, prefer a small diagnostic or guardrail change over a speculative broad rewrite.",
|
|
3289
|
+
"Keep existing style, imports, formatting, and public contracts unless the work item clearly requires a change.",
|
|
2955
3290
|
"Allowed JSON shape:",
|
|
2956
3291
|
"{\"summary\":\"...\",\"files\":[{\"path\":\"relative/path\",\"action\":\"create|update\",\"content\":\"full file content\"}],\"risks\":[\"...\"]}",
|
|
2957
3292
|
"",
|
|
@@ -2964,7 +3299,11 @@ function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
|
2964
3299
|
session.workItem.priority ? `- Priority: ${session.workItem.priority}` : undefined,
|
|
2965
3300
|
session.workItem.labels?.length ? `- Labels: ${session.workItem.labels.join(", ")}` : undefined,
|
|
2966
3301
|
session.workItem.components?.length ? `- Components: ${session.workItem.components.join(", ")}` : undefined,
|
|
3302
|
+
session.workItem.links?.length ? `- Links: ${session.workItem.links.map((link) => `${link.kind}:${link.title ?? link.url}`).join("; ")}` : undefined,
|
|
2967
3303
|
"",
|
|
3304
|
+
missingRequirementSignals.length ? "Known missing or unclear requirements:" : undefined,
|
|
3305
|
+
...missingRequirementSignals.map((signal) => `- ${signal}`),
|
|
3306
|
+
missingRequirementSignals.length ? "" : undefined,
|
|
2968
3307
|
"Approved plan:",
|
|
2969
3308
|
plan?.summary ? `- Summary: ${plan.summary}` : "- Summary: unavailable",
|
|
2970
3309
|
...(plan?.steps.map((step) => `- ${step.title}${step.detail ? `: ${step.detail}` : ""}`) ?? []),
|
|
@@ -2975,7 +3314,13 @@ function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
|
2975
3314
|
failedTestContext.length ? "" : undefined,
|
|
2976
3315
|
"Workspace:",
|
|
2977
3316
|
`- Path: ${workspacePath}`,
|
|
2978
|
-
|
|
3317
|
+
workspace?.name ? `- Name: ${workspace.name}` : undefined,
|
|
3318
|
+
workspace?.currentBranch ? `- Current branch: ${workspace.currentBranch}` : undefined,
|
|
3319
|
+
workspace?.packageNames?.length ? `- Packages: ${workspace.packageNames.slice(0, 8).join(", ")}` : undefined,
|
|
3320
|
+
workspace?.readmeKeywords?.length ? `- README signals: ${workspace.readmeKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
3321
|
+
workspace?.codeownersKeywords?.length ? `- Ownership signals: ${workspace.codeownersKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
3322
|
+
workspace?.recentBranches?.length ? `- Recent branches: ${workspace.recentBranches.slice(0, 8).join(", ")}` : undefined,
|
|
3323
|
+
workspace?.recentCommitRefs?.length ? `- Recent work refs: ${workspace.recentCommitRefs.slice(0, 12).join(", ")}` : undefined,
|
|
2979
3324
|
"",
|
|
2980
3325
|
"Readable context files:",
|
|
2981
3326
|
context || "No source files were safely included. You may propose small new files only if the task clearly asks for them."
|
|
@@ -2986,14 +3331,41 @@ function getFailedTestContextAfterLatestPatch(session) {
|
|
|
2986
3331
|
if (!failedRun) {
|
|
2987
3332
|
return [];
|
|
2988
3333
|
}
|
|
3334
|
+
const rootCauseHint = inferFailedTestRootCause(failedRun);
|
|
2989
3335
|
return [
|
|
2990
3336
|
`- Command: ${failedRun.command}`,
|
|
2991
3337
|
`- Exit code: ${failedRun.exitCode}`,
|
|
3338
|
+
rootCauseHint ? `- Root cause hint: ${rootCauseHint}` : undefined,
|
|
2992
3339
|
failedRun.cwd ? `- Working directory: ${failedRun.cwd}` : undefined,
|
|
2993
3340
|
...failedRun.stdoutSummary.map((line) => `- stdout: ${line}`),
|
|
2994
3341
|
...failedRun.stderrSummary.map((line) => `- stderr: ${line}`)
|
|
2995
3342
|
].filter((line) => Boolean(line)).slice(0, 48);
|
|
2996
3343
|
}
|
|
3344
|
+
function inferFailedTestRootCause(run) {
|
|
3345
|
+
const text = [...run.stdoutSummary, ...run.stderrSummary].join("\n").toLowerCase();
|
|
3346
|
+
if (!text.trim()) {
|
|
3347
|
+
return undefined;
|
|
3348
|
+
}
|
|
3349
|
+
if (/\b(assertionerror|expected .* received|expected .* got|toequal|tobe|assert)\b/su.test(text)) {
|
|
3350
|
+
return "Assertion mismatch; inspect the changed behavior and expected output before editing more files.";
|
|
3351
|
+
}
|
|
3352
|
+
if (/\b(type error|ts\d{4}|typescript|cannot find name|property .* does not exist)\b/su.test(text)) {
|
|
3353
|
+
return "Type or compile failure; prefer the smallest type-safe fix in the impacted file.";
|
|
3354
|
+
}
|
|
3355
|
+
if (/\b(module not found|cannot find module|enoent|no such file|import .* not found)\b/su.test(text)) {
|
|
3356
|
+
return "Missing module/file/import; verify paths, exports, and package boundaries.";
|
|
3357
|
+
}
|
|
3358
|
+
if (/\b(timeout|timed out|exceeded timeout|deadline)\b/su.test(text)) {
|
|
3359
|
+
return "Timeout or hanging validation; inspect async flow, retries, and long-running operations.";
|
|
3360
|
+
}
|
|
3361
|
+
if (/\b(permission denied|eacces|forbidden|unauthorized)\b/su.test(text)) {
|
|
3362
|
+
return "Permission or environment failure; avoid code changes until credentials, filesystem access, or network access are confirmed.";
|
|
3363
|
+
}
|
|
3364
|
+
if (/\b(snapshot|snapshots)\b/su.test(text)) {
|
|
3365
|
+
return "Snapshot mismatch; inspect whether output changed intentionally before updating snapshots.";
|
|
3366
|
+
}
|
|
3367
|
+
return "Validation failed; use the command output to target a narrow repair patch.";
|
|
3368
|
+
}
|
|
2997
3369
|
function parseAIPatchProposal(value, session, provider, workspacePath, createdAt) {
|
|
2998
3370
|
const json = extractJsonObject(value);
|
|
2999
3371
|
if (!json) {
|
|
@@ -3022,7 +3394,7 @@ function parseAIPatchProposal(value, session, provider, workspacePath, createdAt
|
|
|
3022
3394
|
const relativePath = normalizeWorkspaceRelativePath(workspacePath, maybe.path, "throw");
|
|
3023
3395
|
const action = maybe.action === "create" ? "create" : "update";
|
|
3024
3396
|
const content = maybe.content;
|
|
3025
|
-
if (!relativePath || isSensitiveWorkspacePath(relativePath) || content.includes("\u0000")) {
|
|
3397
|
+
if (!relativePath || isSensitiveWorkspacePath(relativePath) || content.includes("\u0000") || containsSensitiveContent(content)) {
|
|
3026
3398
|
return undefined;
|
|
3027
3399
|
}
|
|
3028
3400
|
if (Buffer.byteLength(content, "utf8") > maxPatchProposalBytesPerFile) {
|
|
@@ -3070,6 +3442,9 @@ function isSensitiveWorkspacePath(filePath) {
|
|
|
3070
3442
|
}
|
|
3071
3443
|
return sensitivePathFragments.some((fragment) => lower === fragment || lower.includes(`/${fragment}`) || lower.endsWith(fragment));
|
|
3072
3444
|
}
|
|
3445
|
+
function containsSensitiveContent(content) {
|
|
3446
|
+
return sensitiveContentPatterns.some((pattern) => pattern.test(content));
|
|
3447
|
+
}
|
|
3073
3448
|
async function applyPatchFiles(workspacePath, files) {
|
|
3074
3449
|
for (const file of files) {
|
|
3075
3450
|
const relativePath = normalizeWorkspaceRelativePath(workspacePath, file.path, "throw");
|
|
@@ -3081,7 +3456,7 @@ async function applyPatchFiles(workspacePath, files) {
|
|
|
3081
3456
|
await writeFile(absolutePath, file.content, "utf8");
|
|
3082
3457
|
}
|
|
3083
3458
|
}
|
|
3084
|
-
async function discoverTestCommandCandidates(workspacePath) {
|
|
3459
|
+
async function discoverTestCommandCandidates(workspacePath, session) {
|
|
3085
3460
|
const scripts = await readPackageScripts(join(workspacePath, "package.json"));
|
|
3086
3461
|
const packageManager = detectPackageManager(workspacePath);
|
|
3087
3462
|
const candidates = [];
|
|
@@ -3097,6 +3472,17 @@ async function discoverTestCommandCandidates(workspacePath) {
|
|
|
3097
3472
|
cwd: workspacePath
|
|
3098
3473
|
});
|
|
3099
3474
|
}
|
|
3475
|
+
const relatedTestFiles = session ? await findRelatedTestFiles(workspacePath, session) : [];
|
|
3476
|
+
const testScript = scripts["test"] ? buildPackageScriptCommand(packageManager, "test") : undefined;
|
|
3477
|
+
for (const testFile of relatedTestFiles.slice(0, 5)) {
|
|
3478
|
+
candidates.push({
|
|
3479
|
+
id: `related_${createHash("sha256").update(testFile).digest("hex").slice(0, 8)}`,
|
|
3480
|
+
command: testScript ? `${testScript} -- ${quoteShellArg(testFile)}` : buildLanguageSpecificTestCommand(packageManager, testFile),
|
|
3481
|
+
source: "related_file",
|
|
3482
|
+
reason: `Related test file matched likely impacted work: ${testFile}.`,
|
|
3483
|
+
cwd: workspacePath
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3100
3486
|
if (candidates.length > 0) {
|
|
3101
3487
|
return candidates;
|
|
3102
3488
|
}
|
|
@@ -3113,6 +3499,74 @@ async function discoverTestCommandCandidates(workspacePath) {
|
|
|
3113
3499
|
}
|
|
3114
3500
|
return getFallbackTestCommandCandidates(workspacePath);
|
|
3115
3501
|
}
|
|
3502
|
+
async function findRelatedTestFiles(workspacePath, session) {
|
|
3503
|
+
let trackedFiles = [];
|
|
3504
|
+
try {
|
|
3505
|
+
trackedFiles = await listTrackedWorkspaceFiles(workspacePath);
|
|
3506
|
+
}
|
|
3507
|
+
catch {
|
|
3508
|
+
return [];
|
|
3509
|
+
}
|
|
3510
|
+
const impactHints = new Set([
|
|
3511
|
+
...(session.plan?.filesLikelyChanged ?? [])
|
|
3512
|
+
.map((filePath) => normalizeWorkspaceRelativePath(workspacePath, filePath, "skip"))
|
|
3513
|
+
.filter((filePath) => Boolean(filePath))
|
|
3514
|
+
.flatMap((filePath) => [filePath, basename(filePath).replace(/\.[^.]+$/u, "")]),
|
|
3515
|
+
...tokenizePatchSearchText([
|
|
3516
|
+
session.workItem.key,
|
|
3517
|
+
session.workItem.title,
|
|
3518
|
+
session.workItem.description,
|
|
3519
|
+
...(session.workItem.labels ?? []),
|
|
3520
|
+
...(session.workItem.components ?? []),
|
|
3521
|
+
session.plan?.summary
|
|
3522
|
+
].filter((value) => Boolean(value)).join(" "))
|
|
3523
|
+
]);
|
|
3524
|
+
return trackedFiles
|
|
3525
|
+
.filter((filePath) => isTestLikeFile(filePath))
|
|
3526
|
+
.map((filePath) => ({
|
|
3527
|
+
filePath,
|
|
3528
|
+
score: scoreRelatedTestFile(filePath, impactHints)
|
|
3529
|
+
}))
|
|
3530
|
+
.filter((candidate) => candidate.score > 0)
|
|
3531
|
+
.sort((left, right) => right.score - left.score || left.filePath.localeCompare(right.filePath))
|
|
3532
|
+
.map((candidate) => candidate.filePath);
|
|
3533
|
+
}
|
|
3534
|
+
function isTestLikeFile(filePath) {
|
|
3535
|
+
return /(^|\/)(__tests__|tests?|specs?)\//u.test(filePath.toLowerCase())
|
|
3536
|
+
|| /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py|java|kt|go|rs|rb|php|cs)$/u.test(filePath.toLowerCase());
|
|
3537
|
+
}
|
|
3538
|
+
function scoreRelatedTestFile(filePath, impactHints) {
|
|
3539
|
+
const lower = filePath.toLowerCase();
|
|
3540
|
+
let score = 0;
|
|
3541
|
+
for (const hint of impactHints) {
|
|
3542
|
+
const normalized = hint.toLowerCase();
|
|
3543
|
+
if (normalized.length >= 3 && lower.includes(normalized)) {
|
|
3544
|
+
score += normalized.includes("/") ? 10 : 4;
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
if (score === 0) {
|
|
3548
|
+
return 0;
|
|
3549
|
+
}
|
|
3550
|
+
if (/\.(test|spec)\./u.test(lower)) {
|
|
3551
|
+
score += 3;
|
|
3552
|
+
}
|
|
3553
|
+
if (/(__tests__|tests?)\//u.test(lower)) {
|
|
3554
|
+
score += 2;
|
|
3555
|
+
}
|
|
3556
|
+
if (/(snapshot|fixtures|mocks)\//u.test(lower)) {
|
|
3557
|
+
score -= 2;
|
|
3558
|
+
}
|
|
3559
|
+
return score;
|
|
3560
|
+
}
|
|
3561
|
+
function buildLanguageSpecificTestCommand(packageManager, testFile) {
|
|
3562
|
+
if (/\.(py)$/u.test(testFile)) {
|
|
3563
|
+
return `python -m pytest ${quoteShellArg(testFile)}`;
|
|
3564
|
+
}
|
|
3565
|
+
if (/\.(go)$/u.test(testFile)) {
|
|
3566
|
+
return "go test ./...";
|
|
3567
|
+
}
|
|
3568
|
+
return `${buildPackageScriptCommand(packageManager, "test")} -- ${quoteShellArg(testFile)}`;
|
|
3569
|
+
}
|
|
3116
3570
|
function detectPackageManager(workspacePath) {
|
|
3117
3571
|
if (existsSync(join(workspacePath, "pnpm-lock.yaml"))) {
|
|
3118
3572
|
return "pnpm";
|
|
@@ -3137,6 +3591,9 @@ function buildPackageScriptCommand(packageManager, scriptName) {
|
|
|
3137
3591
|
}
|
|
3138
3592
|
return `pnpm ${scriptName}`;
|
|
3139
3593
|
}
|
|
3594
|
+
function quoteShellArg(value) {
|
|
3595
|
+
return `'${value.replace(/'/gu, "'\\''")}'`;
|
|
3596
|
+
}
|
|
3140
3597
|
function getFallbackTestCommandCandidates(cwd) {
|
|
3141
3598
|
return [
|
|
3142
3599
|
{
|
|
@@ -3237,6 +3694,14 @@ async function ensurePullRequestBranch(workspacePath, branch) {
|
|
|
3237
3694
|
}
|
|
3238
3695
|
return branch;
|
|
3239
3696
|
}
|
|
3697
|
+
async function pushPullRequestBranch(workspacePath, branch) {
|
|
3698
|
+
try {
|
|
3699
|
+
await runGitStrict(workspacePath, ["push", "-u", "origin", branch]);
|
|
3700
|
+
}
|
|
3701
|
+
catch (error) {
|
|
3702
|
+
throw new Error(getGitHubCliGuidance("push pull request branch", error));
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3240
3705
|
async function createGitHubPullRequestWithCli(workspacePath, draft, branch, draftPr) {
|
|
3241
3706
|
const ghArgs = [
|
|
3242
3707
|
"pr",
|
|
@@ -3253,14 +3718,19 @@ async function createGitHubPullRequestWithCli(workspacePath, draft, branch, draf
|
|
|
3253
3718
|
if (draftPr) {
|
|
3254
3719
|
ghArgs.push("--draft");
|
|
3255
3720
|
}
|
|
3256
|
-
|
|
3721
|
+
try {
|
|
3722
|
+
return (await execFileStrict("gh", ghArgs, workspacePath)).trim();
|
|
3723
|
+
}
|
|
3724
|
+
catch (error) {
|
|
3725
|
+
throw new Error(getGitHubCliGuidance("create pull request", error));
|
|
3726
|
+
}
|
|
3257
3727
|
}
|
|
3258
3728
|
async function createGitHubPullRequestWithApi(accessToken, draft, branch, draftPr) {
|
|
3259
3729
|
const repository = parseGitHubRepositoryCoordinates(draft.remoteUrl);
|
|
3260
3730
|
if (!repository) {
|
|
3261
3731
|
throw new Error("Unable to determine GitHub owner/repo from the workspace remote URL.");
|
|
3262
3732
|
}
|
|
3263
|
-
const response = await
|
|
3733
|
+
const response = await fetchGitHub(`${getGitHubApiBaseUrl()}/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/pulls`, {
|
|
3264
3734
|
method: "POST",
|
|
3265
3735
|
headers: {
|
|
3266
3736
|
Accept: "application/vnd.github+json",
|
|
@@ -3275,14 +3745,22 @@ async function createGitHubPullRequestWithApi(accessToken, draft, branch, draftP
|
|
|
3275
3745
|
base: draft.baseBranch,
|
|
3276
3746
|
draft: draftPr
|
|
3277
3747
|
})
|
|
3278
|
-
});
|
|
3748
|
+
}, "create pull request");
|
|
3279
3749
|
if (!response.ok) {
|
|
3280
|
-
|
|
3281
|
-
throw new Error(`GitHub PR creation failed: ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`);
|
|
3750
|
+
throw new Error(await getGitHubStatusGuidance(response, "create pull request"));
|
|
3282
3751
|
}
|
|
3283
3752
|
const payload = (await response.json());
|
|
3284
3753
|
return payload.html_url ?? `https://github.com/${repository.owner}/${repository.repo}/pull/${payload.number ?? ""}`;
|
|
3285
3754
|
}
|
|
3755
|
+
function getGitHubCliGuidance(action, error) {
|
|
3756
|
+
const detail = summarizeExecError(error) ?? String(error);
|
|
3757
|
+
return [
|
|
3758
|
+
`GitHub ${action} failed.`,
|
|
3759
|
+
"Check repository write permission, organization SSO authorization, branch protection, remote URL, and whether your token/SSH key can push to origin.",
|
|
3760
|
+
"Run `pome auth github status` and `git remote -v` to verify the account and repository.",
|
|
3761
|
+
`Detail: ${detail}`
|
|
3762
|
+
].join(" ");
|
|
3763
|
+
}
|
|
3286
3764
|
function parseGitHubRepositoryCoordinates(remoteUrl) {
|
|
3287
3765
|
if (!remoteUrl) {
|
|
3288
3766
|
return undefined;
|
|
@@ -3331,6 +3809,33 @@ async function safeResponseText(response) {
|
|
|
3331
3809
|
return "";
|
|
3332
3810
|
}
|
|
3333
3811
|
}
|
|
3812
|
+
function summarizeProviderBody(value) {
|
|
3813
|
+
const trimmed = value.trim();
|
|
3814
|
+
if (!trimmed) {
|
|
3815
|
+
return "";
|
|
3816
|
+
}
|
|
3817
|
+
try {
|
|
3818
|
+
const parsed = JSON.parse(trimmed);
|
|
3819
|
+
const errorObject = typeof parsed.error === "object" && parsed.error
|
|
3820
|
+
? parsed.error
|
|
3821
|
+
: undefined;
|
|
3822
|
+
const messages = [
|
|
3823
|
+
typeof parsed.message === "string" ? parsed.message : undefined,
|
|
3824
|
+
typeof parsed.error === "string" ? parsed.error : undefined,
|
|
3825
|
+
typeof errorObject?.message === "string" ? errorObject.message : undefined,
|
|
3826
|
+
typeof errorObject?.type === "string" ? `type=${errorObject.type}` : undefined,
|
|
3827
|
+
typeof errorObject?.code === "string" ? `code=${errorObject.code}` : undefined,
|
|
3828
|
+
typeof parsed.documentation_url === "string" ? parsed.documentation_url : undefined
|
|
3829
|
+
].filter((item) => Boolean(item));
|
|
3830
|
+
if (messages.length) {
|
|
3831
|
+
return messages.join("; ").slice(0, 500);
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
catch {
|
|
3835
|
+
// Fall through to plain-text summary.
|
|
3836
|
+
}
|
|
3837
|
+
return trimmed.replace(/\s+/gu, " ").slice(0, 500);
|
|
3838
|
+
}
|
|
3334
3839
|
async function hasWorkspaceChanges(workspacePath) {
|
|
3335
3840
|
const output = await runGit(workspacePath, ["status", "--porcelain"]);
|
|
3336
3841
|
return output.trim().length > 0;
|