@openpome/local-gateway 0.28.0-alpha.0 → 0.32.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 +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +412 -24
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createDefaultWorkItemSourceRegistry, createJiraCloudOAuthLogin, exchang
|
|
|
15
15
|
const execAsync = promisify(exec);
|
|
16
16
|
const execFileAsync = promisify(execFile);
|
|
17
17
|
const jiraOAuthCredentialAccount = "jira-cloud/oauth";
|
|
18
|
+
const githubOAuthCredentialAccount = "github/oauth";
|
|
18
19
|
const openAiCredentialAccount = "model/openai/api-key";
|
|
19
20
|
const anthropicCredentialAccount = "model/anthropic/api-key";
|
|
20
21
|
const workspaceIndexFileName = "workspace-index.json";
|
|
@@ -39,7 +40,7 @@ const maxWorkspaceScanRepositories = 200;
|
|
|
39
40
|
export function getGatewayHealth() {
|
|
40
41
|
return {
|
|
41
42
|
status: "ok",
|
|
42
|
-
version: "0.
|
|
43
|
+
version: "0.32.0-alpha.0"
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
export async function initOpenPome() {
|
|
@@ -463,7 +464,7 @@ export async function startTaskSession(key, env = process.env) {
|
|
|
463
464
|
}
|
|
464
465
|
export async function getTaskSessionStatus() {
|
|
465
466
|
const paths = getOpenPomePaths();
|
|
466
|
-
const persisted = await
|
|
467
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
467
468
|
if (!persisted) {
|
|
468
469
|
return {
|
|
469
470
|
active: false,
|
|
@@ -494,7 +495,7 @@ export async function getTaskSessionStatus() {
|
|
|
494
495
|
}
|
|
495
496
|
export async function getTaskSessionTimeline() {
|
|
496
497
|
const paths = getOpenPomePaths();
|
|
497
|
-
const persisted = await
|
|
498
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
498
499
|
return {
|
|
499
500
|
active: Boolean(persisted),
|
|
500
501
|
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
@@ -504,7 +505,7 @@ export async function getTaskSessionTimeline() {
|
|
|
504
505
|
}
|
|
505
506
|
export async function getTaskSessionApprovalHistory() {
|
|
506
507
|
const paths = getOpenPomePaths();
|
|
507
|
-
const persisted = await
|
|
508
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
508
509
|
return {
|
|
509
510
|
active: Boolean(persisted),
|
|
510
511
|
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
@@ -638,7 +639,7 @@ export async function resetTaskSession() {
|
|
|
638
639
|
}
|
|
639
640
|
export async function createTaskSessionPlan() {
|
|
640
641
|
const paths = getOpenPomePaths();
|
|
641
|
-
const persisted = await
|
|
642
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
642
643
|
if (!persisted) {
|
|
643
644
|
return undefined;
|
|
644
645
|
}
|
|
@@ -686,7 +687,7 @@ export async function createTaskSessionPlan() {
|
|
|
686
687
|
}
|
|
687
688
|
export async function approveTaskSessionPlan() {
|
|
688
689
|
const paths = getOpenPomePaths();
|
|
689
|
-
const persisted = await
|
|
690
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
690
691
|
if (!persisted) {
|
|
691
692
|
return undefined;
|
|
692
693
|
}
|
|
@@ -730,7 +731,7 @@ export async function approveTaskSessionPlan() {
|
|
|
730
731
|
}
|
|
731
732
|
export async function rejectTaskSessionPlan(reason = "Plan rejected by developer.") {
|
|
732
733
|
const paths = getOpenPomePaths();
|
|
733
|
-
const persisted = await
|
|
734
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
734
735
|
if (!persisted) {
|
|
735
736
|
return undefined;
|
|
736
737
|
}
|
|
@@ -774,7 +775,7 @@ export async function rejectTaskSessionPlan(reason = "Plan rejected by developer
|
|
|
774
775
|
}
|
|
775
776
|
export async function createAIPatchProposal() {
|
|
776
777
|
const paths = getOpenPomePaths();
|
|
777
|
-
const persisted = await
|
|
778
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
778
779
|
if (!persisted) {
|
|
779
780
|
return {
|
|
780
781
|
active: false,
|
|
@@ -808,18 +809,21 @@ export async function createAIPatchProposal() {
|
|
|
808
809
|
throw new Error("Manual-copy mode cannot apply code. Run `pome auth ai openai`, `pome auth ai claude`, or `pome auth ai claude-cli` to enable approval-gated AI patches.");
|
|
809
810
|
}
|
|
810
811
|
const createdAt = new Date().toISOString();
|
|
812
|
+
const retryingFailedTest = hasFailedTestAfterLatestAppliedPatch(persisted);
|
|
811
813
|
const fileContext = await collectPatchContextFiles(workspacePath, persisted);
|
|
812
814
|
const prompt = buildStructuredPatchPrompt(persisted, workspacePath, fileContext);
|
|
813
815
|
const response = await completeModelText(provider, prompt);
|
|
814
816
|
const proposalDraft = parseAIPatchProposal(response, persisted, provider, workspacePath, createdAt);
|
|
815
|
-
const approval = createFileEditApproval(persisted, proposalDraft, createdAt,
|
|
817
|
+
const approval = createFileEditApproval(persisted, proposalDraft, createdAt, retryingFailedTest
|
|
818
|
+
? "Developer approval is required before OpenPome writes AI-proposed fixes for the failed test."
|
|
819
|
+
: "Developer approval is required before OpenPome writes AI-proposed file changes.");
|
|
816
820
|
const proposal = {
|
|
817
821
|
...proposalDraft,
|
|
818
822
|
approval
|
|
819
823
|
};
|
|
820
824
|
const session = {
|
|
821
825
|
...persisted.session,
|
|
822
|
-
status: "awaiting_approval",
|
|
826
|
+
status: retryingFailedTest ? "fixing" : "awaiting_approval",
|
|
823
827
|
updatedAt: createdAt
|
|
824
828
|
};
|
|
825
829
|
await writeActiveTaskSession(paths.homeDirectory, {
|
|
@@ -828,7 +832,7 @@ export async function createAIPatchProposal() {
|
|
|
828
832
|
aiPatchProposal: proposal,
|
|
829
833
|
approvalHistory: appendApprovalHistory(persisted.approvalHistory, approval),
|
|
830
834
|
events: appendSessionEvents(persisted.events, [
|
|
831
|
-
createSessionEvent(session, persisted.workItem.key, "approval_requested", "AI file changes proposed", createdAt, [
|
|
835
|
+
createSessionEvent(session, persisted.workItem.key, "approval_requested", retryingFailedTest ? "AI test-failure fix proposed" : "AI file changes proposed", createdAt, [
|
|
832
836
|
`Provider: ${getModelProviderDisplayName(provider)}`,
|
|
833
837
|
`Files proposed: ${proposal.files.map((file) => file.path).join(", ") || "none"}`,
|
|
834
838
|
...approval.details
|
|
@@ -849,7 +853,7 @@ export async function createAIPatchProposal() {
|
|
|
849
853
|
}
|
|
850
854
|
export async function approveAndApplyAIPatchProposal() {
|
|
851
855
|
const paths = getOpenPomePaths();
|
|
852
|
-
const persisted = await
|
|
856
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
853
857
|
if (!persisted) {
|
|
854
858
|
return undefined;
|
|
855
859
|
}
|
|
@@ -919,7 +923,7 @@ export async function approveAndApplyAIPatchProposal() {
|
|
|
919
923
|
}
|
|
920
924
|
export async function discoverTestCommands() {
|
|
921
925
|
const paths = getOpenPomePaths();
|
|
922
|
-
const persisted = await
|
|
926
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
923
927
|
if (!persisted) {
|
|
924
928
|
return {
|
|
925
929
|
active: false,
|
|
@@ -953,7 +957,7 @@ export async function discoverTestCommands() {
|
|
|
953
957
|
}
|
|
954
958
|
export async function approveTestCommand(command) {
|
|
955
959
|
const paths = getOpenPomePaths();
|
|
956
|
-
const persisted = await
|
|
960
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
957
961
|
if (!persisted) {
|
|
958
962
|
return undefined;
|
|
959
963
|
}
|
|
@@ -996,7 +1000,7 @@ export async function approveTestCommand(command) {
|
|
|
996
1000
|
}
|
|
997
1001
|
export async function getTestCommandHistory() {
|
|
998
1002
|
const paths = getOpenPomePaths();
|
|
999
|
-
const persisted = await
|
|
1003
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
1000
1004
|
return {
|
|
1001
1005
|
active: Boolean(persisted),
|
|
1002
1006
|
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
@@ -1007,7 +1011,7 @@ export async function getTestCommandHistory() {
|
|
|
1007
1011
|
}
|
|
1008
1012
|
export async function runApprovedTestCommand(command) {
|
|
1009
1013
|
const paths = getOpenPomePaths();
|
|
1010
|
-
const persisted = await
|
|
1014
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
1011
1015
|
if (!persisted) {
|
|
1012
1016
|
return undefined;
|
|
1013
1017
|
}
|
|
@@ -1049,7 +1053,7 @@ export async function runApprovedTestCommand(command) {
|
|
|
1049
1053
|
}
|
|
1050
1054
|
export async function createManualCopyAIContext() {
|
|
1051
1055
|
const paths = getOpenPomePaths();
|
|
1052
|
-
const persisted = await
|
|
1056
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
1053
1057
|
if (!persisted) {
|
|
1054
1058
|
return {
|
|
1055
1059
|
active: false,
|
|
@@ -1083,7 +1087,7 @@ export async function createManualCopyAIContext() {
|
|
|
1083
1087
|
}
|
|
1084
1088
|
export async function createManualCopyAIPrompt() {
|
|
1085
1089
|
const paths = getOpenPomePaths();
|
|
1086
|
-
const persisted = await
|
|
1090
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
1087
1091
|
if (!persisted) {
|
|
1088
1092
|
return {
|
|
1089
1093
|
active: false,
|
|
@@ -1119,7 +1123,7 @@ export async function createManualCopyAIPrompt() {
|
|
|
1119
1123
|
}
|
|
1120
1124
|
export async function getDiffSummary() {
|
|
1121
1125
|
const paths = getOpenPomePaths();
|
|
1122
|
-
const persisted = await
|
|
1126
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
1123
1127
|
if (!persisted) {
|
|
1124
1128
|
return {
|
|
1125
1129
|
active: false,
|
|
@@ -1150,6 +1154,106 @@ export async function getDiffSummary() {
|
|
|
1150
1154
|
};
|
|
1151
1155
|
}
|
|
1152
1156
|
export async function getGitHubAuthStatus() {
|
|
1157
|
+
const storedToken = await readStoredGitHubOAuth();
|
|
1158
|
+
if (storedToken?.accessToken) {
|
|
1159
|
+
try {
|
|
1160
|
+
const user = await fetchGitHubAuthenticatedUser(storedToken.accessToken);
|
|
1161
|
+
return {
|
|
1162
|
+
provider: "github",
|
|
1163
|
+
cliAvailable: await isGitHubCliAvailable(),
|
|
1164
|
+
cliAuthenticated: await isGitHubCliAuthenticated(),
|
|
1165
|
+
nativeAuthenticated: true,
|
|
1166
|
+
authenticated: true,
|
|
1167
|
+
username: user.login,
|
|
1168
|
+
tokenSource: "openpome",
|
|
1169
|
+
detail: `OpenPome GitHub browser login is connected as ${user.login}.`
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
catch (error) {
|
|
1173
|
+
const fallback = await getGitHubCliAuthStatus();
|
|
1174
|
+
if (fallback.authenticated) {
|
|
1175
|
+
return {
|
|
1176
|
+
...fallback,
|
|
1177
|
+
detail: `OpenPome GitHub token could not be verified (${summarizeUnknownError(error)}). ${fallback.detail}`
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
return {
|
|
1181
|
+
...fallback,
|
|
1182
|
+
detail: `OpenPome GitHub token could not be verified (${summarizeUnknownError(error)}). Run \`pome auth github login\` again.`
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return getGitHubCliAuthStatus();
|
|
1187
|
+
}
|
|
1188
|
+
export async function createGitHubDeviceLogin(env = process.env) {
|
|
1189
|
+
const clientId = getGitHubOAuthClientId(env);
|
|
1190
|
+
const scope = getGitHubOAuthScope(env);
|
|
1191
|
+
const body = new URLSearchParams({
|
|
1192
|
+
client_id: clientId,
|
|
1193
|
+
scope
|
|
1194
|
+
});
|
|
1195
|
+
const response = await fetch("https://github.com/login/device/code", {
|
|
1196
|
+
method: "POST",
|
|
1197
|
+
headers: {
|
|
1198
|
+
Accept: "application/json",
|
|
1199
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1200
|
+
},
|
|
1201
|
+
body
|
|
1202
|
+
});
|
|
1203
|
+
if (!response.ok) {
|
|
1204
|
+
throw new Error(`GitHub device login failed: ${response.status} ${response.statusText}`);
|
|
1205
|
+
}
|
|
1206
|
+
const payload = (await response.json());
|
|
1207
|
+
if (!payload.device_code || !payload.user_code || !payload.verification_uri) {
|
|
1208
|
+
throw new Error("GitHub device login response was incomplete.");
|
|
1209
|
+
}
|
|
1210
|
+
const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : 900;
|
|
1211
|
+
const intervalSeconds = Math.max(1, typeof payload.interval === "number" ? payload.interval : 5);
|
|
1212
|
+
return {
|
|
1213
|
+
provider: "github",
|
|
1214
|
+
deviceCode: payload.device_code,
|
|
1215
|
+
userCode: payload.user_code,
|
|
1216
|
+
verificationUri: payload.verification_uri,
|
|
1217
|
+
expiresIn,
|
|
1218
|
+
expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),
|
|
1219
|
+
intervalSeconds,
|
|
1220
|
+
scope,
|
|
1221
|
+
detail: "Open the GitHub verification URL, enter the code, then keep this terminal open."
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
export async function completeGitHubDeviceLogin(login, env = process.env, options = {}) {
|
|
1225
|
+
const clientId = getGitHubOAuthClientId(env);
|
|
1226
|
+
const deadline = Date.now() + login.expiresIn * 1000;
|
|
1227
|
+
let intervalSeconds = login.intervalSeconds;
|
|
1228
|
+
while (Date.now() < deadline) {
|
|
1229
|
+
await delay(options.pollDelayMilliseconds ?? intervalSeconds * 1000);
|
|
1230
|
+
const token = await pollGitHubDeviceAccessToken(clientId, login.deviceCode);
|
|
1231
|
+
if (token.status === "pending") {
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
if (token.status === "slow_down") {
|
|
1235
|
+
intervalSeconds += 5;
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
if (token.status === "error") {
|
|
1239
|
+
throw new Error(token.detail);
|
|
1240
|
+
}
|
|
1241
|
+
const store = createCredentialStore();
|
|
1242
|
+
if (!store.isAvailable()) {
|
|
1243
|
+
throw new Error(`Credential store is unavailable: ${store.backend}`);
|
|
1244
|
+
}
|
|
1245
|
+
await setJsonCredential(store, githubOAuthCredentialAccount, token.token);
|
|
1246
|
+
const user = await fetchGitHubAuthenticatedUser(token.token.accessToken);
|
|
1247
|
+
return {
|
|
1248
|
+
provider: "github",
|
|
1249
|
+
authenticated: true,
|
|
1250
|
+
username: user.login,
|
|
1251
|
+
detail: `GitHub browser login completed for ${user.login}.`
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
throw new Error("Timed out waiting for GitHub browser login approval.");
|
|
1255
|
+
}
|
|
1256
|
+
async function getGitHubCliAuthStatus() {
|
|
1153
1257
|
try {
|
|
1154
1258
|
await execFileAsync("gh", ["--version"]);
|
|
1155
1259
|
}
|
|
@@ -1157,7 +1261,10 @@ export async function getGitHubAuthStatus() {
|
|
|
1157
1261
|
return {
|
|
1158
1262
|
provider: "github",
|
|
1159
1263
|
cliAvailable: false,
|
|
1264
|
+
cliAuthenticated: false,
|
|
1265
|
+
nativeAuthenticated: false,
|
|
1160
1266
|
authenticated: false,
|
|
1267
|
+
tokenSource: "none",
|
|
1161
1268
|
detail: "GitHub CLI is not installed or is not on PATH."
|
|
1162
1269
|
};
|
|
1163
1270
|
}
|
|
@@ -1166,7 +1273,10 @@ export async function getGitHubAuthStatus() {
|
|
|
1166
1273
|
return {
|
|
1167
1274
|
provider: "github",
|
|
1168
1275
|
cliAvailable: true,
|
|
1276
|
+
cliAuthenticated: true,
|
|
1277
|
+
nativeAuthenticated: false,
|
|
1169
1278
|
authenticated: true,
|
|
1279
|
+
tokenSource: "github-cli",
|
|
1170
1280
|
detail: "GitHub CLI is authenticated for github.com."
|
|
1171
1281
|
};
|
|
1172
1282
|
}
|
|
@@ -1174,14 +1284,17 @@ export async function getGitHubAuthStatus() {
|
|
|
1174
1284
|
return {
|
|
1175
1285
|
provider: "github",
|
|
1176
1286
|
cliAvailable: true,
|
|
1287
|
+
cliAuthenticated: false,
|
|
1288
|
+
nativeAuthenticated: false,
|
|
1177
1289
|
authenticated: false,
|
|
1290
|
+
tokenSource: "none",
|
|
1178
1291
|
detail: summarizeExecError(error) || "GitHub CLI is installed but not authenticated for github.com."
|
|
1179
1292
|
};
|
|
1180
1293
|
}
|
|
1181
1294
|
}
|
|
1182
1295
|
export async function createPullRequestDraft() {
|
|
1183
1296
|
const paths = getOpenPomePaths();
|
|
1184
|
-
const persisted = await
|
|
1297
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
1185
1298
|
if (!persisted) {
|
|
1186
1299
|
return {
|
|
1187
1300
|
active: false,
|
|
@@ -1212,7 +1325,7 @@ export async function createPullRequestDraft() {
|
|
|
1212
1325
|
}
|
|
1213
1326
|
export async function createPullRequest(options = {}) {
|
|
1214
1327
|
const paths = getOpenPomePaths();
|
|
1215
|
-
const persisted = await
|
|
1328
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
1216
1329
|
if (!persisted) {
|
|
1217
1330
|
return {
|
|
1218
1331
|
active: false,
|
|
@@ -1310,7 +1423,7 @@ export async function createPullRequest(options = {}) {
|
|
|
1310
1423
|
}
|
|
1311
1424
|
export async function createWorkItemUpdateDraft() {
|
|
1312
1425
|
const paths = getOpenPomePaths();
|
|
1313
|
-
const persisted = await
|
|
1426
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
1314
1427
|
if (!persisted) {
|
|
1315
1428
|
return {
|
|
1316
1429
|
active: false,
|
|
@@ -1339,7 +1452,7 @@ export async function createWorkItemUpdateDraft() {
|
|
|
1339
1452
|
}
|
|
1340
1453
|
export async function postWorkItemUpdate() {
|
|
1341
1454
|
const paths = getOpenPomePaths();
|
|
1342
|
-
const persisted = await
|
|
1455
|
+
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
1343
1456
|
if (!persisted) {
|
|
1344
1457
|
return {
|
|
1345
1458
|
active: false,
|
|
@@ -1614,6 +1727,122 @@ async function readStoredJiraOAuth() {
|
|
|
1614
1727
|
}
|
|
1615
1728
|
return getJsonCredential(store, jiraOAuthCredentialAccount);
|
|
1616
1729
|
}
|
|
1730
|
+
async function readStoredGitHubOAuth() {
|
|
1731
|
+
const store = createCredentialStore();
|
|
1732
|
+
if (!store.isAvailable()) {
|
|
1733
|
+
return undefined;
|
|
1734
|
+
}
|
|
1735
|
+
return getJsonCredential(store, githubOAuthCredentialAccount);
|
|
1736
|
+
}
|
|
1737
|
+
function getGitHubOAuthClientId(env) {
|
|
1738
|
+
const clientId = env["OPENPOME_GITHUB_OAUTH_CLIENT_ID"]?.trim();
|
|
1739
|
+
if (!clientId) {
|
|
1740
|
+
throw new Error("OPENPOME_GITHUB_OAUTH_CLIENT_ID is required for native GitHub browser login. Without it, use `gh auth login` as the fallback.");
|
|
1741
|
+
}
|
|
1742
|
+
return clientId;
|
|
1743
|
+
}
|
|
1744
|
+
function getGitHubOAuthScope(env) {
|
|
1745
|
+
return env["OPENPOME_GITHUB_OAUTH_SCOPE"]?.trim() || "repo read:user";
|
|
1746
|
+
}
|
|
1747
|
+
async function isGitHubCliAvailable() {
|
|
1748
|
+
try {
|
|
1749
|
+
await execFileAsync("gh", ["--version"]);
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1752
|
+
catch {
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
async function isGitHubCliAuthenticated() {
|
|
1757
|
+
try {
|
|
1758
|
+
await execFileAsync("gh", ["auth", "status", "-h", "github.com"]);
|
|
1759
|
+
return true;
|
|
1760
|
+
}
|
|
1761
|
+
catch {
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
async function fetchGitHubAuthenticatedUser(accessToken) {
|
|
1766
|
+
const response = await fetch("https://api.github.com/user", {
|
|
1767
|
+
headers: {
|
|
1768
|
+
Accept: "application/vnd.github+json",
|
|
1769
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1770
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
if (!response.ok) {
|
|
1774
|
+
throw new Error(`GitHub user lookup failed: ${response.status} ${response.statusText}`);
|
|
1775
|
+
}
|
|
1776
|
+
const payload = (await response.json());
|
|
1777
|
+
if (!payload.login) {
|
|
1778
|
+
throw new Error("GitHub user lookup response was incomplete.");
|
|
1779
|
+
}
|
|
1780
|
+
return {
|
|
1781
|
+
login: payload.login,
|
|
1782
|
+
id: payload.id,
|
|
1783
|
+
name: payload.name
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
async function pollGitHubDeviceAccessToken(clientId, deviceCode) {
|
|
1787
|
+
const response = await fetch("https://github.com/login/oauth/access_token", {
|
|
1788
|
+
method: "POST",
|
|
1789
|
+
headers: {
|
|
1790
|
+
Accept: "application/json",
|
|
1791
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1792
|
+
},
|
|
1793
|
+
body: new URLSearchParams({
|
|
1794
|
+
client_id: clientId,
|
|
1795
|
+
device_code: deviceCode,
|
|
1796
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
1797
|
+
})
|
|
1798
|
+
});
|
|
1799
|
+
if (!response.ok) {
|
|
1800
|
+
return {
|
|
1801
|
+
status: "error",
|
|
1802
|
+
detail: `GitHub device token polling failed: ${response.status} ${response.statusText}`
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
const payload = (await response.json());
|
|
1806
|
+
if (payload.error) {
|
|
1807
|
+
if (payload.error === "authorization_pending") {
|
|
1808
|
+
return { status: "pending" };
|
|
1809
|
+
}
|
|
1810
|
+
if (payload.error === "slow_down") {
|
|
1811
|
+
return { status: "slow_down" };
|
|
1812
|
+
}
|
|
1813
|
+
return {
|
|
1814
|
+
status: "error",
|
|
1815
|
+
detail: payload.error_description || `GitHub device login failed: ${payload.error}`
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
if (!payload.access_token) {
|
|
1819
|
+
return {
|
|
1820
|
+
status: "error",
|
|
1821
|
+
detail: "GitHub device token response did not include an access token."
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
return {
|
|
1825
|
+
status: "complete",
|
|
1826
|
+
token: {
|
|
1827
|
+
accessToken: payload.access_token,
|
|
1828
|
+
tokenType: payload.token_type || "bearer",
|
|
1829
|
+
scopes: parseGitHubScopes(payload.scope),
|
|
1830
|
+
createdAt: new Date().toISOString()
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
function parseGitHubScopes(scope) {
|
|
1835
|
+
return (scope ?? "")
|
|
1836
|
+
.split(",")
|
|
1837
|
+
.map((value) => value.trim())
|
|
1838
|
+
.filter((value) => value.length > 0);
|
|
1839
|
+
}
|
|
1840
|
+
function summarizeUnknownError(error) {
|
|
1841
|
+
return error instanceof Error ? error.message : String(error);
|
|
1842
|
+
}
|
|
1843
|
+
function delay(milliseconds) {
|
|
1844
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1845
|
+
}
|
|
1617
1846
|
async function refreshStoredJiraOAuthIfNeeded(tokenSet, env) {
|
|
1618
1847
|
if (!tokenSet || !shouldRefreshOAuthToken(tokenSet)) {
|
|
1619
1848
|
return tokenSet;
|
|
@@ -1999,6 +2228,134 @@ function appendSessionEvents(existing, events) {
|
|
|
1999
2228
|
function appendApprovalHistory(existing, approval) {
|
|
2000
2229
|
return [...(existing ?? []), approval];
|
|
2001
2230
|
}
|
|
2231
|
+
async function refreshActiveTaskSessionWorkItem(homeDirectory) {
|
|
2232
|
+
const persisted = await readActiveTaskSessionIfPresent(homeDirectory);
|
|
2233
|
+
if (!persisted) {
|
|
2234
|
+
return undefined;
|
|
2235
|
+
}
|
|
2236
|
+
return refreshPersistedTaskSessionWorkItem(homeDirectory, persisted);
|
|
2237
|
+
}
|
|
2238
|
+
async function refreshPersistedTaskSessionWorkItem(homeDirectory, persisted) {
|
|
2239
|
+
let latest;
|
|
2240
|
+
try {
|
|
2241
|
+
const source = await createJiraSource(process.env);
|
|
2242
|
+
latest = await source.getWorkItem(persisted.workItem.key);
|
|
2243
|
+
}
|
|
2244
|
+
catch {
|
|
2245
|
+
return persisted;
|
|
2246
|
+
}
|
|
2247
|
+
if (!latest) {
|
|
2248
|
+
return persisted;
|
|
2249
|
+
}
|
|
2250
|
+
const previousFullFingerprint = getWorkItemFullFingerprint(persisted.workItem);
|
|
2251
|
+
const latestFullFingerprint = getWorkItemFullFingerprint(latest);
|
|
2252
|
+
if (previousFullFingerprint === latestFullFingerprint) {
|
|
2253
|
+
return persisted;
|
|
2254
|
+
}
|
|
2255
|
+
const materialChanged = getWorkItemMaterialFingerprint(persisted.workItem) !== getWorkItemMaterialFingerprint(latest);
|
|
2256
|
+
const shouldInvalidate = materialChanged && persisted.session.status !== "completed";
|
|
2257
|
+
const now = new Date().toISOString();
|
|
2258
|
+
const session = {
|
|
2259
|
+
...persisted.session,
|
|
2260
|
+
status: shouldInvalidate ? "planning" : persisted.session.status,
|
|
2261
|
+
updatedAt: now
|
|
2262
|
+
};
|
|
2263
|
+
const refreshDetails = [
|
|
2264
|
+
"OpenPome refreshed the active story from Jira before continuing.",
|
|
2265
|
+
...summarizeWorkItemRefreshChanges(persisted.workItem, latest),
|
|
2266
|
+
...(shouldInvalidate
|
|
2267
|
+
? ["Story scope changed, so the current plan and pending AI outputs were reset."]
|
|
2268
|
+
: ["Only non-planning fields changed; existing plan state was kept."])
|
|
2269
|
+
];
|
|
2270
|
+
const common = {
|
|
2271
|
+
version: persisted.version,
|
|
2272
|
+
session,
|
|
2273
|
+
workItem: latest,
|
|
2274
|
+
workspaceCandidate: persisted.workspaceCandidate,
|
|
2275
|
+
events: appendSessionEvents(persisted.events, [
|
|
2276
|
+
createSessionEvent(session, latest.key, "work_item_refreshed", "Jira story refreshed", now, refreshDetails, {
|
|
2277
|
+
materialChanged: String(materialChanged),
|
|
2278
|
+
previousStatus: persisted.workItem.status,
|
|
2279
|
+
latestStatus: latest.status
|
|
2280
|
+
})
|
|
2281
|
+
]),
|
|
2282
|
+
approvalHistory: persisted.approvalHistory ?? [],
|
|
2283
|
+
prCreation: persisted.prCreation,
|
|
2284
|
+
workItemUpdatePost: persisted.workItemUpdatePost
|
|
2285
|
+
};
|
|
2286
|
+
const refreshed = shouldInvalidate
|
|
2287
|
+
? common
|
|
2288
|
+
: {
|
|
2289
|
+
...persisted,
|
|
2290
|
+
session,
|
|
2291
|
+
workItem: latest,
|
|
2292
|
+
events: common.events
|
|
2293
|
+
};
|
|
2294
|
+
await writeActiveTaskSession(homeDirectory, refreshed);
|
|
2295
|
+
return refreshed;
|
|
2296
|
+
}
|
|
2297
|
+
function getWorkItemMaterialFingerprint(item) {
|
|
2298
|
+
return stableStringify({
|
|
2299
|
+
key: item.key,
|
|
2300
|
+
source: item.source,
|
|
2301
|
+
type: item.type,
|
|
2302
|
+
title: item.title,
|
|
2303
|
+
description: item.description ?? "",
|
|
2304
|
+
priority: item.priority ?? "",
|
|
2305
|
+
iteration: item.iteration ?? "",
|
|
2306
|
+
parentKey: item.parentKey ?? "",
|
|
2307
|
+
labels: normalizeStringList(item.labels),
|
|
2308
|
+
components: normalizeStringList(item.components),
|
|
2309
|
+
links: normalizeWorkItemLinks(item.links),
|
|
2310
|
+
subtasks: normalizeWorkItemSummaries(item.subtasks)
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
function getWorkItemFullFingerprint(item) {
|
|
2314
|
+
return stableStringify({
|
|
2315
|
+
material: JSON.parse(getWorkItemMaterialFingerprint(item)),
|
|
2316
|
+
status: item.status,
|
|
2317
|
+
assignee: item.assignee ?? ""
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
function summarizeWorkItemRefreshChanges(previous, latest) {
|
|
2321
|
+
const changes = [
|
|
2322
|
+
previous.title !== latest.title ? "Title changed." : undefined,
|
|
2323
|
+
(previous.description ?? "") !== (latest.description ?? "") ? "Description or acceptance criteria changed." : undefined,
|
|
2324
|
+
previous.status !== latest.status ? `Status changed: ${previous.status} -> ${latest.status}.` : undefined,
|
|
2325
|
+
(previous.priority ?? "") !== (latest.priority ?? "") ? `Priority changed: ${previous.priority ?? "none"} -> ${latest.priority ?? "none"}.` : undefined,
|
|
2326
|
+
(previous.assignee ?? "") !== (latest.assignee ?? "") ? `Assignee changed: ${previous.assignee ?? "none"} -> ${latest.assignee ?? "none"}.` : undefined,
|
|
2327
|
+
stableStringify(normalizeStringList(previous.labels)) !== stableStringify(normalizeStringList(latest.labels)) ? "Labels changed." : undefined,
|
|
2328
|
+
stableStringify(normalizeStringList(previous.components)) !== stableStringify(normalizeStringList(latest.components)) ? "Components changed." : undefined,
|
|
2329
|
+
stableStringify(normalizeWorkItemLinks(previous.links)) !== stableStringify(normalizeWorkItemLinks(latest.links)) ? "Linked work changed." : undefined,
|
|
2330
|
+
stableStringify(normalizeWorkItemSummaries(previous.subtasks)) !== stableStringify(normalizeWorkItemSummaries(latest.subtasks)) ? "Subtasks changed." : undefined
|
|
2331
|
+
].filter((change) => Boolean(change));
|
|
2332
|
+
return changes.length ? changes.slice(0, 8) : ["Jira returned updated story metadata."];
|
|
2333
|
+
}
|
|
2334
|
+
function normalizeStringList(values) {
|
|
2335
|
+
return [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right));
|
|
2336
|
+
}
|
|
2337
|
+
function normalizeWorkItemLinks(links) {
|
|
2338
|
+
return (links ?? [])
|
|
2339
|
+
.map((link) => ({
|
|
2340
|
+
kind: link.kind,
|
|
2341
|
+
url: link.url,
|
|
2342
|
+
title: link.title ?? ""
|
|
2343
|
+
}))
|
|
2344
|
+
.sort((left, right) => `${left.kind}:${left.url}:${left.title}`.localeCompare(`${right.kind}:${right.url}:${right.title}`));
|
|
2345
|
+
}
|
|
2346
|
+
function normalizeWorkItemSummaries(summaries) {
|
|
2347
|
+
return (summaries ?? [])
|
|
2348
|
+
.map((summary) => ({
|
|
2349
|
+
key: summary.key,
|
|
2350
|
+
type: summary.type,
|
|
2351
|
+
title: summary.title,
|
|
2352
|
+
status: summary.status
|
|
2353
|
+
}))
|
|
2354
|
+
.sort((left, right) => left.key.localeCompare(right.key));
|
|
2355
|
+
}
|
|
2356
|
+
function stableStringify(value) {
|
|
2357
|
+
return JSON.stringify(value);
|
|
2358
|
+
}
|
|
2002
2359
|
function createWorkspaceId(path) {
|
|
2003
2360
|
const hash = createHash("sha256").update(path).digest("hex").slice(0, 12);
|
|
2004
2361
|
return `${basename(path).toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "")}-${hash}`;
|
|
@@ -2433,6 +2790,7 @@ function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
|
2433
2790
|
file.content,
|
|
2434
2791
|
"```"
|
|
2435
2792
|
].join("\n")).join("\n\n");
|
|
2793
|
+
const failedTestContext = getFailedTestContextAfterLatestPatch(session);
|
|
2436
2794
|
return [
|
|
2437
2795
|
"You are OpenPome's implementation engine.",
|
|
2438
2796
|
"Return only compact JSON. Do not include markdown fences outside JSON.",
|
|
@@ -2457,6 +2815,9 @@ function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
|
2457
2815
|
...(plan?.steps.map((step) => `- ${step.title}${step.detail ? `: ${step.detail}` : ""}`) ?? []),
|
|
2458
2816
|
plan?.commandsToRun.length ? `- Checks: ${plan.commandsToRun.join(", ")}` : undefined,
|
|
2459
2817
|
"",
|
|
2818
|
+
failedTestContext.length ? "Recent failed validation after the latest approved patch:" : undefined,
|
|
2819
|
+
...failedTestContext,
|
|
2820
|
+
failedTestContext.length ? "" : undefined,
|
|
2460
2821
|
"Workspace:",
|
|
2461
2822
|
`- Path: ${workspacePath}`,
|
|
2462
2823
|
session.workspaceCandidate?.workspace.name ? `- Name: ${session.workspaceCandidate.workspace.name}` : undefined,
|
|
@@ -2465,6 +2826,19 @@ function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
|
2465
2826
|
context || "No source files were safely included. You may propose small new files only if the task clearly asks for them."
|
|
2466
2827
|
].filter((line) => Boolean(line)).join("\n");
|
|
2467
2828
|
}
|
|
2829
|
+
function getFailedTestContextAfterLatestPatch(session) {
|
|
2830
|
+
const failedRun = getLatestFailedTestRunAfterLatestPatch(session);
|
|
2831
|
+
if (!failedRun) {
|
|
2832
|
+
return [];
|
|
2833
|
+
}
|
|
2834
|
+
return [
|
|
2835
|
+
`- Command: ${failedRun.command}`,
|
|
2836
|
+
`- Exit code: ${failedRun.exitCode}`,
|
|
2837
|
+
failedRun.cwd ? `- Working directory: ${failedRun.cwd}` : undefined,
|
|
2838
|
+
...failedRun.stdoutSummary.map((line) => `- stdout: ${line}`),
|
|
2839
|
+
...failedRun.stderrSummary.map((line) => `- stderr: ${line}`)
|
|
2840
|
+
].filter((line) => Boolean(line)).slice(0, 48);
|
|
2841
|
+
}
|
|
2468
2842
|
function parseAIPatchProposal(value, session, provider, workspacePath, createdAt) {
|
|
2469
2843
|
const json = extractJsonObject(value);
|
|
2470
2844
|
if (!json) {
|
|
@@ -2726,7 +3100,21 @@ async function detectPullRequestBaseBranch(workspacePath) {
|
|
|
2726
3100
|
return headBranch || "main";
|
|
2727
3101
|
}
|
|
2728
3102
|
function hasPassedTestEvidence(session) {
|
|
2729
|
-
|
|
3103
|
+
const latestRunAfterPatch = getLatestTestRunAfterLatestPatch(session);
|
|
3104
|
+
return latestRunAfterPatch?.status === "passed" || (!session.aiPatchProposal?.appliedAt && (session.testRunEvidence ?? []).some((run) => run.status === "passed"));
|
|
3105
|
+
}
|
|
3106
|
+
function hasFailedTestAfterLatestAppliedPatch(session) {
|
|
3107
|
+
return Boolean(getLatestFailedTestRunAfterLatestPatch(session));
|
|
3108
|
+
}
|
|
3109
|
+
function getLatestFailedTestRunAfterLatestPatch(session) {
|
|
3110
|
+
const latestRun = getLatestTestRunAfterLatestPatch(session);
|
|
3111
|
+
return latestRun?.status === "failed" ? latestRun : undefined;
|
|
3112
|
+
}
|
|
3113
|
+
function getLatestTestRunAfterLatestPatch(session) {
|
|
3114
|
+
const appliedAt = session.aiPatchProposal?.appliedAt;
|
|
3115
|
+
const runs = session.testRunEvidence ?? [];
|
|
3116
|
+
const filtered = appliedAt ? runs.filter((run) => run.finishedAt >= appliedAt) : runs;
|
|
3117
|
+
return filtered[filtered.length - 1];
|
|
2730
3118
|
}
|
|
2731
3119
|
async function runGitStrict(cwd, args) {
|
|
2732
3120
|
return execFileStrict("git", args, cwd);
|