@openpome/local-gateway 0.23.0-alpha.0 → 0.26.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/connectors/work-item-registry.d.ts +3 -2
- package/dist/connectors/work-item-registry.d.ts.map +1 -1
- package/dist/connectors/work-item-registry.js +3 -0
- package/dist/connectors/work-item-registry.js.map +1 -1
- package/dist/index.d.ts +68 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +630 -32
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { mkdir, opendir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import { defaultConfig } from "@openpome/configuration";
|
|
10
10
|
import { createCredentialStore, getJsonCredential, setJsonCredential } from "@openpome/credentials";
|
|
@@ -39,7 +39,7 @@ const maxWorkspaceScanRepositories = 200;
|
|
|
39
39
|
export function getGatewayHealth() {
|
|
40
40
|
return {
|
|
41
41
|
status: "ok",
|
|
42
|
-
version: "0.
|
|
42
|
+
version: "0.26.0-alpha.0"
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
export async function initOpenPome() {
|
|
@@ -465,8 +465,11 @@ export async function getTaskSessionStatus() {
|
|
|
465
465
|
testRunEvidence: persisted.testRunEvidence ?? [],
|
|
466
466
|
prDraft: persisted.prDraft,
|
|
467
467
|
workItemUpdateDraft: persisted.workItemUpdateDraft,
|
|
468
|
+
prCreation: persisted.prCreation,
|
|
469
|
+
workItemUpdatePost: persisted.workItemUpdatePost,
|
|
468
470
|
aiContext: persisted.aiContext,
|
|
469
|
-
diffSummary: persisted.diffSummary
|
|
471
|
+
diffSummary: persisted.diffSummary,
|
|
472
|
+
aiPatchProposal: persisted.aiPatchProposal
|
|
470
473
|
};
|
|
471
474
|
}
|
|
472
475
|
export async function getTaskSessionTimeline() {
|
|
@@ -749,6 +752,157 @@ export async function rejectTaskSessionPlan(reason = "Plan rejected by developer
|
|
|
749
752
|
nextStep: "Revise the work item context or workspace link, then run `pome plan` again."
|
|
750
753
|
};
|
|
751
754
|
}
|
|
755
|
+
export async function createAIPatchProposal() {
|
|
756
|
+
const paths = getOpenPomePaths();
|
|
757
|
+
const persisted = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
758
|
+
if (!persisted) {
|
|
759
|
+
return {
|
|
760
|
+
active: false,
|
|
761
|
+
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
762
|
+
nextStep: "Run `pome start <KEY>` first."
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
if (persisted.aiPatchProposal && !persisted.aiPatchProposal.appliedAt) {
|
|
766
|
+
return {
|
|
767
|
+
active: true,
|
|
768
|
+
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
769
|
+
session: persisted.session,
|
|
770
|
+
proposal: persisted.aiPatchProposal,
|
|
771
|
+
workspacePath: persisted.workspaceCandidate?.workspace.path,
|
|
772
|
+
nextStep: "Review the proposed file changes, then run `pome approve` to apply them."
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
if (!persisted.plan) {
|
|
776
|
+
throw new Error("No implementation plan is available. Run `pome plan` first.");
|
|
777
|
+
}
|
|
778
|
+
if (persisted.planApproval?.status !== "approved") {
|
|
779
|
+
throw new Error("The implementation plan is not approved yet. Run `pome approve` first.");
|
|
780
|
+
}
|
|
781
|
+
const workspacePath = persisted.workspaceCandidate?.workspace.path;
|
|
782
|
+
if (!workspacePath) {
|
|
783
|
+
throw new Error("No workspace path is available for AI implementation. Open the repo and run `pome start <KEY>` again, or link it with `pome workspace link <KEY> <PATH>`.");
|
|
784
|
+
}
|
|
785
|
+
const config = await readConfigIfPresent(paths.configFile);
|
|
786
|
+
const provider = normalizeModelProviderId(config?.activeModelProvider ?? defaultConfig.activeModelProvider);
|
|
787
|
+
if (provider === "manual-copy") {
|
|
788
|
+
throw new Error("Manual-copy mode cannot apply code. Run `pome auth ai openai` or `pome auth ai claude` to enable approval-gated AI patches.");
|
|
789
|
+
}
|
|
790
|
+
const apiKey = await getModelProviderApiKey(provider);
|
|
791
|
+
if (!apiKey) {
|
|
792
|
+
throw new Error(`${getModelProviderDisplayName(provider)} is active, but no API key is configured. Run \`pome auth ai ${provider === "anthropic" ? "claude" : provider}\`.`);
|
|
793
|
+
}
|
|
794
|
+
const createdAt = new Date().toISOString();
|
|
795
|
+
const fileContext = await collectPatchContextFiles(workspacePath, persisted);
|
|
796
|
+
const prompt = buildStructuredPatchPrompt(persisted, workspacePath, fileContext);
|
|
797
|
+
const response = provider === "openai"
|
|
798
|
+
? await completeOpenAIText(prompt, apiKey)
|
|
799
|
+
: await completeAnthropicText(prompt, apiKey);
|
|
800
|
+
const proposalDraft = parseAIPatchProposal(response, persisted, provider, workspacePath, createdAt);
|
|
801
|
+
const approval = createFileEditApproval(persisted, proposalDraft, createdAt, "Developer approval is required before OpenPome writes AI-proposed file changes.");
|
|
802
|
+
const proposal = {
|
|
803
|
+
...proposalDraft,
|
|
804
|
+
approval
|
|
805
|
+
};
|
|
806
|
+
const session = {
|
|
807
|
+
...persisted.session,
|
|
808
|
+
status: "awaiting_approval",
|
|
809
|
+
updatedAt: createdAt
|
|
810
|
+
};
|
|
811
|
+
await writeActiveTaskSession(paths.homeDirectory, {
|
|
812
|
+
...persisted,
|
|
813
|
+
session,
|
|
814
|
+
aiPatchProposal: proposal,
|
|
815
|
+
approvalHistory: appendApprovalHistory(persisted.approvalHistory, approval),
|
|
816
|
+
events: appendSessionEvents(persisted.events, [
|
|
817
|
+
createSessionEvent(session, persisted.workItem.key, "approval_requested", "AI file changes proposed", createdAt, [
|
|
818
|
+
`Provider: ${getModelProviderDisplayName(provider)}`,
|
|
819
|
+
`Files proposed: ${proposal.files.map((file) => file.path).join(", ") || "none"}`,
|
|
820
|
+
...approval.details
|
|
821
|
+
], {
|
|
822
|
+
approvalId: approval.id,
|
|
823
|
+
approvalType: approval.type
|
|
824
|
+
})
|
|
825
|
+
])
|
|
826
|
+
});
|
|
827
|
+
return {
|
|
828
|
+
active: true,
|
|
829
|
+
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
830
|
+
session,
|
|
831
|
+
proposal,
|
|
832
|
+
workspacePath,
|
|
833
|
+
nextStep: "Review the proposed file changes, then run `pome approve` to apply them."
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
export async function approveAndApplyAIPatchProposal() {
|
|
837
|
+
const paths = getOpenPomePaths();
|
|
838
|
+
const persisted = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
839
|
+
if (!persisted) {
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
const proposal = persisted.aiPatchProposal;
|
|
843
|
+
if (!proposal) {
|
|
844
|
+
throw new Error("No AI file changes are waiting for approval. Run `pome next` first.");
|
|
845
|
+
}
|
|
846
|
+
if (proposal.appliedAt) {
|
|
847
|
+
return {
|
|
848
|
+
active: true,
|
|
849
|
+
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
850
|
+
session: persisted.session,
|
|
851
|
+
proposal,
|
|
852
|
+
summary: persisted.diffSummary,
|
|
853
|
+
nextStep: "Run `pome done` to prepare the PR and Jira update drafts."
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
const workspacePath = persisted.workspaceCandidate?.workspace.path;
|
|
857
|
+
if (!workspacePath) {
|
|
858
|
+
throw new Error("No workspace path is available for the active task session.");
|
|
859
|
+
}
|
|
860
|
+
const now = new Date().toISOString();
|
|
861
|
+
const approvedProposal = {
|
|
862
|
+
...proposal,
|
|
863
|
+
approval: {
|
|
864
|
+
...proposal.approval,
|
|
865
|
+
status: "approved"
|
|
866
|
+
},
|
|
867
|
+
appliedAt: now
|
|
868
|
+
};
|
|
869
|
+
await applyPatchFiles(workspacePath, approvedProposal.files);
|
|
870
|
+
const summary = await buildDiffSummary(workspacePath, now);
|
|
871
|
+
const session = {
|
|
872
|
+
...persisted.session,
|
|
873
|
+
status: "implementing",
|
|
874
|
+
updatedAt: now
|
|
875
|
+
};
|
|
876
|
+
await writeActiveTaskSession(paths.homeDirectory, {
|
|
877
|
+
...persisted,
|
|
878
|
+
session,
|
|
879
|
+
aiPatchProposal: approvedProposal,
|
|
880
|
+
diffSummary: summary,
|
|
881
|
+
approvalHistory: appendApprovalHistory(persisted.approvalHistory, approvedProposal.approval),
|
|
882
|
+
events: appendSessionEvents(persisted.events, [
|
|
883
|
+
createSessionEvent(session, persisted.workItem.key, "approval_approved", "AI file changes approved and applied", now, [
|
|
884
|
+
`Files applied: ${approvedProposal.files.map((file) => file.path).join(", ")}`,
|
|
885
|
+
`Changed files in git diff: ${summary.files.length}`
|
|
886
|
+
], {
|
|
887
|
+
approvalId: approvedProposal.approval.id,
|
|
888
|
+
approvalType: approvedProposal.approval.type
|
|
889
|
+
}),
|
|
890
|
+
createSessionEvent(session, persisted.workItem.key, "session_status_changed", "AI patch applied", now, [
|
|
891
|
+
"OpenPome wrote only the approved files and captured a diff summary."
|
|
892
|
+
], {
|
|
893
|
+
status: session.status
|
|
894
|
+
})
|
|
895
|
+
])
|
|
896
|
+
});
|
|
897
|
+
return {
|
|
898
|
+
active: true,
|
|
899
|
+
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
900
|
+
session,
|
|
901
|
+
proposal: approvedProposal,
|
|
902
|
+
summary,
|
|
903
|
+
nextStep: "Review the diff, run approved tests, then run `pome done`."
|
|
904
|
+
};
|
|
905
|
+
}
|
|
752
906
|
export async function discoverTestCommands() {
|
|
753
907
|
const paths = getOpenPomePaths();
|
|
754
908
|
const persisted = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
@@ -1011,12 +1165,6 @@ export async function getGitHubAuthStatus() {
|
|
|
1011
1165
|
};
|
|
1012
1166
|
}
|
|
1013
1167
|
}
|
|
1014
|
-
export async function createPullRequestExternalGuard() {
|
|
1015
|
-
return createExternalActionGuard("create_pr");
|
|
1016
|
-
}
|
|
1017
|
-
export async function postWorkItemUpdateExternalGuard() {
|
|
1018
|
-
return createExternalActionGuard("update_work_item");
|
|
1019
|
-
}
|
|
1020
1168
|
export async function createPullRequestDraft() {
|
|
1021
1169
|
const paths = getOpenPomePaths();
|
|
1022
1170
|
const persisted = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
@@ -1027,7 +1175,9 @@ export async function createPullRequestDraft() {
|
|
|
1027
1175
|
};
|
|
1028
1176
|
}
|
|
1029
1177
|
const now = new Date().toISOString();
|
|
1030
|
-
const draft = buildPullRequestDraft(persisted, now
|
|
1178
|
+
const draft = buildPullRequestDraft(persisted, now, persisted.workspaceCandidate?.workspace.path
|
|
1179
|
+
? await detectPullRequestBaseBranch(persisted.workspaceCandidate.workspace.path)
|
|
1180
|
+
: "main");
|
|
1031
1181
|
await writeActiveTaskSession(paths.homeDirectory, {
|
|
1032
1182
|
...persisted,
|
|
1033
1183
|
prDraft: draft,
|
|
@@ -1046,6 +1196,104 @@ export async function createPullRequestDraft() {
|
|
|
1046
1196
|
draft
|
|
1047
1197
|
};
|
|
1048
1198
|
}
|
|
1199
|
+
export async function createPullRequest(options = {}) {
|
|
1200
|
+
const paths = getOpenPomePaths();
|
|
1201
|
+
const persisted = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
1202
|
+
if (!persisted) {
|
|
1203
|
+
return {
|
|
1204
|
+
active: false,
|
|
1205
|
+
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
1206
|
+
pushed: false,
|
|
1207
|
+
draftPr: Boolean(options.draft)
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
if (persisted.planApproval?.status !== "approved") {
|
|
1211
|
+
throw new Error("Plan approval is required before creating a PR. Run `pome approve` first.");
|
|
1212
|
+
}
|
|
1213
|
+
const workspacePath = persisted.workspaceCandidate?.workspace.path;
|
|
1214
|
+
if (!workspacePath) {
|
|
1215
|
+
throw new Error("No workspace path is available for PR creation.");
|
|
1216
|
+
}
|
|
1217
|
+
if (!persisted.diffSummary) {
|
|
1218
|
+
throw new Error("Review the final diff summary before creating a PR. Run `pome diff` first.");
|
|
1219
|
+
}
|
|
1220
|
+
if (!options.allowUntested && !hasPassedTestEvidence(persisted)) {
|
|
1221
|
+
throw new Error("Passed test evidence is required before creating a PR. Run `pome test discover`, `pome approve command`, and `pome test run`, or pass `--allow-untested`.");
|
|
1222
|
+
}
|
|
1223
|
+
const github = await getGitHubAuthStatus();
|
|
1224
|
+
if (!github.authenticated) {
|
|
1225
|
+
throw new Error(`${github.detail} Run \`pome auth github login\` first.`);
|
|
1226
|
+
}
|
|
1227
|
+
const now = new Date().toISOString();
|
|
1228
|
+
const baseBranch = options.baseBranch?.trim() || await detectPullRequestBaseBranch(workspacePath);
|
|
1229
|
+
const draft = {
|
|
1230
|
+
...(persisted.prDraft ?? buildPullRequestDraft(persisted, now, baseBranch)),
|
|
1231
|
+
baseBranch
|
|
1232
|
+
};
|
|
1233
|
+
const branch = await ensurePullRequestBranch(workspacePath, draft.headBranch);
|
|
1234
|
+
const commitMessage = `${persisted.workItem.key}: ${persisted.workItem.title}`;
|
|
1235
|
+
const hasChanges = await hasWorkspaceChanges(workspacePath);
|
|
1236
|
+
if (!hasChanges) {
|
|
1237
|
+
throw new Error("No local changes are available to commit for this PR.");
|
|
1238
|
+
}
|
|
1239
|
+
await runGitStrict(workspacePath, ["add", "-A"]);
|
|
1240
|
+
await runGitStrict(workspacePath, ["commit", "-m", commitMessage]);
|
|
1241
|
+
await runGitStrict(workspacePath, ["push", "-u", "origin", branch]);
|
|
1242
|
+
const ghArgs = [
|
|
1243
|
+
"pr",
|
|
1244
|
+
"create",
|
|
1245
|
+
"--title",
|
|
1246
|
+
draft.title,
|
|
1247
|
+
"--body",
|
|
1248
|
+
draft.body,
|
|
1249
|
+
"--base",
|
|
1250
|
+
draft.baseBranch,
|
|
1251
|
+
"--head",
|
|
1252
|
+
branch
|
|
1253
|
+
];
|
|
1254
|
+
if (options.draft) {
|
|
1255
|
+
ghArgs.push("--draft");
|
|
1256
|
+
}
|
|
1257
|
+
const prUrl = (await execFileStrict("gh", ghArgs, workspacePath)).trim();
|
|
1258
|
+
const approval = createExternalActionApproval(persisted, "create_pr", now, [
|
|
1259
|
+
`Branch: ${branch}`,
|
|
1260
|
+
`Commit: ${commitMessage}`,
|
|
1261
|
+
`PR: ${prUrl || "created"}`,
|
|
1262
|
+
options.draft ? "Draft PR: yes" : "Draft PR: no"
|
|
1263
|
+
]);
|
|
1264
|
+
const result = {
|
|
1265
|
+
active: true,
|
|
1266
|
+
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
1267
|
+
session: persisted.session,
|
|
1268
|
+
draft,
|
|
1269
|
+
approval,
|
|
1270
|
+
prUrl: prUrl || undefined,
|
|
1271
|
+
branch,
|
|
1272
|
+
commitMessage,
|
|
1273
|
+
pushed: true,
|
|
1274
|
+
draftPr: Boolean(options.draft),
|
|
1275
|
+
createdAt: now
|
|
1276
|
+
};
|
|
1277
|
+
await writeActiveTaskSession(paths.homeDirectory, {
|
|
1278
|
+
...persisted,
|
|
1279
|
+
prDraft: draft,
|
|
1280
|
+
prCreation: result,
|
|
1281
|
+
approvalHistory: appendApprovalHistory(persisted.approvalHistory, approval),
|
|
1282
|
+
events: appendSessionEvents(persisted.events, [
|
|
1283
|
+
createSessionEvent(persisted.session, persisted.workItem.key, "approval_approved", options.draft ? "GitHub draft PR created" : "GitHub PR created", now, [
|
|
1284
|
+
`Branch: ${branch}`,
|
|
1285
|
+
prUrl ? `PR: ${prUrl}` : "PR created by GitHub CLI",
|
|
1286
|
+
options.draft ? "Draft PR: yes" : "Draft PR: no"
|
|
1287
|
+
], {
|
|
1288
|
+
approvalId: approval.id,
|
|
1289
|
+
approvalType: approval.type,
|
|
1290
|
+
branch,
|
|
1291
|
+
prUrl
|
|
1292
|
+
})
|
|
1293
|
+
])
|
|
1294
|
+
});
|
|
1295
|
+
return result;
|
|
1296
|
+
}
|
|
1049
1297
|
export async function createWorkItemUpdateDraft() {
|
|
1050
1298
|
const paths = getOpenPomePaths();
|
|
1051
1299
|
const persisted = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
@@ -1075,6 +1323,61 @@ export async function createWorkItemUpdateDraft() {
|
|
|
1075
1323
|
draft
|
|
1076
1324
|
};
|
|
1077
1325
|
}
|
|
1326
|
+
export async function postWorkItemUpdate() {
|
|
1327
|
+
const paths = getOpenPomePaths();
|
|
1328
|
+
const persisted = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
1329
|
+
if (!persisted) {
|
|
1330
|
+
return {
|
|
1331
|
+
active: false,
|
|
1332
|
+
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
1333
|
+
posted: false
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
if (persisted.planApproval?.status !== "approved") {
|
|
1337
|
+
throw new Error("Plan approval is required before posting a work item update. Run `pome approve` first.");
|
|
1338
|
+
}
|
|
1339
|
+
const now = new Date().toISOString();
|
|
1340
|
+
const draft = persisted.workItemUpdateDraft ?? buildWorkItemUpdateDraft(persisted, now);
|
|
1341
|
+
const source = await createJiraSource(process.env);
|
|
1342
|
+
if (!source.postUpdate) {
|
|
1343
|
+
throw new Error(`Work item source ${source.displayName} does not support posting updates yet.`);
|
|
1344
|
+
}
|
|
1345
|
+
const posted = await source.postUpdate(persisted.workItem.key, draft.body);
|
|
1346
|
+
const approval = createExternalActionApproval(persisted, "update_work_item", now, [
|
|
1347
|
+
`Work item: ${persisted.workItem.key}`,
|
|
1348
|
+
`Comment: ${posted.commentId ?? "posted"}`,
|
|
1349
|
+
posted.self ? `URL: ${posted.self}` : "URL: unavailable"
|
|
1350
|
+
]);
|
|
1351
|
+
const result = {
|
|
1352
|
+
active: true,
|
|
1353
|
+
sessionFile: getActiveTaskSessionFile(paths.homeDirectory),
|
|
1354
|
+
session: persisted.session,
|
|
1355
|
+
workItem: persisted.workItem,
|
|
1356
|
+
draft,
|
|
1357
|
+
approval,
|
|
1358
|
+
posted: true,
|
|
1359
|
+
commentId: posted.commentId,
|
|
1360
|
+
url: posted.self,
|
|
1361
|
+
postedAt: posted.createdAt ?? now
|
|
1362
|
+
};
|
|
1363
|
+
await writeActiveTaskSession(paths.homeDirectory, {
|
|
1364
|
+
...persisted,
|
|
1365
|
+
workItemUpdateDraft: draft,
|
|
1366
|
+
workItemUpdatePost: result,
|
|
1367
|
+
approvalHistory: appendApprovalHistory(persisted.approvalHistory, approval),
|
|
1368
|
+
events: appendSessionEvents(persisted.events, [
|
|
1369
|
+
createSessionEvent(persisted.session, persisted.workItem.key, "approval_approved", "Work item update posted", now, [
|
|
1370
|
+
`Work item: ${persisted.workItem.key}`,
|
|
1371
|
+
posted.commentId ? `Comment: ${posted.commentId}` : "Comment posted"
|
|
1372
|
+
], {
|
|
1373
|
+
approvalId: approval.id,
|
|
1374
|
+
approvalType: approval.type,
|
|
1375
|
+
commentId: posted.commentId ?? ""
|
|
1376
|
+
})
|
|
1377
|
+
])
|
|
1378
|
+
});
|
|
1379
|
+
return result;
|
|
1380
|
+
}
|
|
1078
1381
|
export async function getJiraAuthStatus(env = process.env) {
|
|
1079
1382
|
const source = await createJiraSource(env);
|
|
1080
1383
|
const status = source.getAuthStatus();
|
|
@@ -1808,7 +2111,7 @@ function buildInitialImplementationPlan(workItem, workspaceCandidate) {
|
|
|
1808
2111
|
commandsToRun: ["pome approve", "pnpm validate"],
|
|
1809
2112
|
risks: [
|
|
1810
2113
|
"Workspace resolution may be incomplete until real GitHub and historical session signals are added.",
|
|
1811
|
-
"
|
|
2114
|
+
"Manual-copy mode uses deterministic planning; connect OpenAI or Claude for AI-assisted planning and patch proposals."
|
|
1812
2115
|
],
|
|
1813
2116
|
missingInfo: hasWorkspace ? [] : ["No workspace candidate is selected yet."]
|
|
1814
2117
|
};
|
|
@@ -1829,6 +2132,9 @@ async function buildImplementationPlan(persisted, prompt) {
|
|
|
1829
2132
|
return parseImplementationPlan(response, persisted.workItem, persisted.workspaceCandidate, provider);
|
|
1830
2133
|
}
|
|
1831
2134
|
async function completeOpenAIPlan(prompt, apiKey) {
|
|
2135
|
+
return completeOpenAIText(buildStructuredPlanPrompt(prompt), apiKey);
|
|
2136
|
+
}
|
|
2137
|
+
async function completeOpenAIText(prompt, apiKey) {
|
|
1832
2138
|
const response = await fetch("https://api.openai.com/v1/responses", {
|
|
1833
2139
|
method: "POST",
|
|
1834
2140
|
headers: {
|
|
@@ -1837,11 +2143,11 @@ async function completeOpenAIPlan(prompt, apiKey) {
|
|
|
1837
2143
|
},
|
|
1838
2144
|
body: JSON.stringify({
|
|
1839
2145
|
model: process.env["OPENPOME_OPENAI_MODEL"] ?? "gpt-5",
|
|
1840
|
-
input:
|
|
2146
|
+
input: prompt
|
|
1841
2147
|
})
|
|
1842
2148
|
});
|
|
1843
2149
|
if (!response.ok) {
|
|
1844
|
-
throw new Error(`OpenAI
|
|
2150
|
+
throw new Error(`OpenAI request failed: ${response.status} ${response.statusText}`);
|
|
1845
2151
|
}
|
|
1846
2152
|
const body = await response.json();
|
|
1847
2153
|
if (typeof body.output_text === "string") {
|
|
@@ -1855,6 +2161,9 @@ async function completeOpenAIPlan(prompt, apiKey) {
|
|
|
1855
2161
|
.join("\n");
|
|
1856
2162
|
}
|
|
1857
2163
|
async function completeAnthropicPlan(prompt, apiKey) {
|
|
2164
|
+
return completeAnthropicText(buildStructuredPlanPrompt(prompt), apiKey);
|
|
2165
|
+
}
|
|
2166
|
+
async function completeAnthropicText(prompt, apiKey) {
|
|
1858
2167
|
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
1859
2168
|
method: "POST",
|
|
1860
2169
|
headers: {
|
|
@@ -1868,13 +2177,13 @@ async function completeAnthropicPlan(prompt, apiKey) {
|
|
|
1868
2177
|
messages: [
|
|
1869
2178
|
{
|
|
1870
2179
|
role: "user",
|
|
1871
|
-
content:
|
|
2180
|
+
content: prompt
|
|
1872
2181
|
}
|
|
1873
2182
|
]
|
|
1874
2183
|
})
|
|
1875
2184
|
});
|
|
1876
2185
|
if (!response.ok) {
|
|
1877
|
-
throw new Error(`Claude
|
|
2186
|
+
throw new Error(`Claude request failed: ${response.status} ${response.statusText}`);
|
|
1878
2187
|
}
|
|
1879
2188
|
const body = await response.json();
|
|
1880
2189
|
const content = Array.isArray(body.content) ? body.content : [];
|
|
@@ -1938,6 +2247,220 @@ function extractJsonObject(value) {
|
|
|
1938
2247
|
function stringArrayOr(value, fallback) {
|
|
1939
2248
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : fallback;
|
|
1940
2249
|
}
|
|
2250
|
+
const maxPatchContextFiles = 12;
|
|
2251
|
+
const maxPatchContextBytesPerFile = 16 * 1024;
|
|
2252
|
+
const maxPatchContextTotalBytes = 64 * 1024;
|
|
2253
|
+
const maxPatchProposalFiles = 8;
|
|
2254
|
+
const maxPatchProposalBytesPerFile = 256 * 1024;
|
|
2255
|
+
const sensitivePathFragments = [
|
|
2256
|
+
".env",
|
|
2257
|
+
".npmrc",
|
|
2258
|
+
".pypirc",
|
|
2259
|
+
".netrc",
|
|
2260
|
+
".ssh",
|
|
2261
|
+
".aws",
|
|
2262
|
+
".gcp",
|
|
2263
|
+
".azure",
|
|
2264
|
+
"id_rsa",
|
|
2265
|
+
"id_dsa",
|
|
2266
|
+
"id_ed25519"
|
|
2267
|
+
];
|
|
2268
|
+
async function collectPatchContextFiles(workspacePath, session) {
|
|
2269
|
+
const candidates = [];
|
|
2270
|
+
for (const filePath of session.plan?.filesLikelyChanged ?? []) {
|
|
2271
|
+
const normalized = normalizeWorkspaceRelativePath(workspacePath, filePath, "skip");
|
|
2272
|
+
if (normalized && normalized !== ".") {
|
|
2273
|
+
candidates.push(normalized);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
candidates.push("package.json", "README.md", "AGENTS.md");
|
|
2277
|
+
const trackedFiles = await listTrackedWorkspaceFiles(workspacePath);
|
|
2278
|
+
const tokens = tokenizePatchSearchText([
|
|
2279
|
+
session.workItem.key,
|
|
2280
|
+
session.workItem.title,
|
|
2281
|
+
session.workItem.description,
|
|
2282
|
+
...(session.workItem.labels ?? []),
|
|
2283
|
+
...(session.workItem.components ?? [])
|
|
2284
|
+
].filter((value) => Boolean(value)).join(" "));
|
|
2285
|
+
for (const filePath of trackedFiles) {
|
|
2286
|
+
const lower = filePath.toLowerCase();
|
|
2287
|
+
if (tokens.some((token) => lower.includes(token))) {
|
|
2288
|
+
candidates.push(filePath);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
const selected = [];
|
|
2292
|
+
const seen = new Set();
|
|
2293
|
+
let totalBytes = 0;
|
|
2294
|
+
for (const candidate of candidates) {
|
|
2295
|
+
if (selected.length >= maxPatchContextFiles || totalBytes >= maxPatchContextTotalBytes) {
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
const relativePath = normalizeWorkspaceRelativePath(workspacePath, candidate, "skip");
|
|
2299
|
+
if (!relativePath || seen.has(relativePath) || isSensitiveWorkspacePath(relativePath)) {
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
seen.add(relativePath);
|
|
2303
|
+
const absolutePath = resolve(workspacePath, relativePath);
|
|
2304
|
+
try {
|
|
2305
|
+
const content = await readFile(absolutePath, "utf8");
|
|
2306
|
+
if (content.includes("\u0000")) {
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
const remainingBytes = maxPatchContextTotalBytes - totalBytes;
|
|
2310
|
+
const maxBytes = Math.min(maxPatchContextBytesPerFile, remainingBytes);
|
|
2311
|
+
const sliced = content.slice(0, maxBytes);
|
|
2312
|
+
totalBytes += Buffer.byteLength(sliced, "utf8");
|
|
2313
|
+
selected.push({
|
|
2314
|
+
path: relativePath,
|
|
2315
|
+
content: sliced,
|
|
2316
|
+
truncated: Buffer.byteLength(content, "utf8") > Buffer.byteLength(sliced, "utf8")
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
catch {
|
|
2320
|
+
// Missing files from the AI plan are still useful as create candidates, but not as context.
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
return selected;
|
|
2324
|
+
}
|
|
2325
|
+
async function listTrackedWorkspaceFiles(workspacePath) {
|
|
2326
|
+
const output = await runGit(workspacePath, ["ls-files"]);
|
|
2327
|
+
return output
|
|
2328
|
+
.split(/\r?\n/u)
|
|
2329
|
+
.map((line) => line.trim())
|
|
2330
|
+
.filter(Boolean)
|
|
2331
|
+
.filter((filePath) => !isSensitiveWorkspacePath(filePath))
|
|
2332
|
+
.slice(0, 1000);
|
|
2333
|
+
}
|
|
2334
|
+
function tokenizePatchSearchText(value) {
|
|
2335
|
+
return Array.from(new Set(value.toLowerCase().split(/[^a-z0-9]+/u).filter((token) => token.length >= 3))).slice(0, 20);
|
|
2336
|
+
}
|
|
2337
|
+
function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
2338
|
+
const plan = session.plan;
|
|
2339
|
+
const context = contextFiles.map((file) => [
|
|
2340
|
+
`FILE: ${file.path}${file.truncated ? " (truncated)" : ""}`,
|
|
2341
|
+
"```",
|
|
2342
|
+
file.content,
|
|
2343
|
+
"```"
|
|
2344
|
+
].join("\n")).join("\n\n");
|
|
2345
|
+
return [
|
|
2346
|
+
"You are OpenPome's implementation engine.",
|
|
2347
|
+
"Return only compact JSON. Do not include markdown fences outside JSON.",
|
|
2348
|
+
"Only propose a minimal safe file patch for the approved work item.",
|
|
2349
|
+
"Do not include secrets, credentials, generated dependency folders, lockfile rewrites, or unrelated refactors.",
|
|
2350
|
+
"Use full replacement file content for each proposed file.",
|
|
2351
|
+
"Allowed JSON shape:",
|
|
2352
|
+
"{\"summary\":\"...\",\"files\":[{\"path\":\"relative/path\",\"action\":\"create|update\",\"content\":\"full file content\"}],\"risks\":[\"...\"]}",
|
|
2353
|
+
"",
|
|
2354
|
+
"Work item:",
|
|
2355
|
+
`- Key: ${session.workItem.key}`,
|
|
2356
|
+
`- Type: ${session.workItem.type}`,
|
|
2357
|
+
`- Status: ${session.workItem.status}`,
|
|
2358
|
+
`- Title: ${session.workItem.title}`,
|
|
2359
|
+
session.workItem.description ? `- Description: ${session.workItem.description}` : undefined,
|
|
2360
|
+
session.workItem.priority ? `- Priority: ${session.workItem.priority}` : undefined,
|
|
2361
|
+
session.workItem.labels?.length ? `- Labels: ${session.workItem.labels.join(", ")}` : undefined,
|
|
2362
|
+
session.workItem.components?.length ? `- Components: ${session.workItem.components.join(", ")}` : undefined,
|
|
2363
|
+
"",
|
|
2364
|
+
"Approved plan:",
|
|
2365
|
+
plan?.summary ? `- Summary: ${plan.summary}` : "- Summary: unavailable",
|
|
2366
|
+
...(plan?.steps.map((step) => `- ${step.title}${step.detail ? `: ${step.detail}` : ""}`) ?? []),
|
|
2367
|
+
plan?.commandsToRun.length ? `- Checks: ${plan.commandsToRun.join(", ")}` : undefined,
|
|
2368
|
+
"",
|
|
2369
|
+
"Workspace:",
|
|
2370
|
+
`- Path: ${workspacePath}`,
|
|
2371
|
+
session.workspaceCandidate?.workspace.name ? `- Name: ${session.workspaceCandidate.workspace.name}` : undefined,
|
|
2372
|
+
"",
|
|
2373
|
+
"Readable context files:",
|
|
2374
|
+
context || "No source files were safely included. You may propose small new files only if the task clearly asks for them."
|
|
2375
|
+
].filter((line) => Boolean(line)).join("\n");
|
|
2376
|
+
}
|
|
2377
|
+
function parseAIPatchProposal(value, session, provider, workspacePath, createdAt) {
|
|
2378
|
+
const json = extractJsonObject(value);
|
|
2379
|
+
if (!json) {
|
|
2380
|
+
throw new Error(`${getModelProviderDisplayName(provider)} did not return a JSON patch proposal.`);
|
|
2381
|
+
}
|
|
2382
|
+
let parsed;
|
|
2383
|
+
try {
|
|
2384
|
+
parsed = JSON.parse(json);
|
|
2385
|
+
}
|
|
2386
|
+
catch {
|
|
2387
|
+
throw new Error(`${getModelProviderDisplayName(provider)} returned invalid JSON for the patch proposal.`);
|
|
2388
|
+
}
|
|
2389
|
+
if (!Array.isArray(parsed.files)) {
|
|
2390
|
+
throw new Error(`${getModelProviderDisplayName(provider)} patch proposal did not include files.`);
|
|
2391
|
+
}
|
|
2392
|
+
const files = parsed.files
|
|
2393
|
+
.slice(0, maxPatchProposalFiles)
|
|
2394
|
+
.map((file) => {
|
|
2395
|
+
if (typeof file !== "object" || !file) {
|
|
2396
|
+
return undefined;
|
|
2397
|
+
}
|
|
2398
|
+
const maybe = file;
|
|
2399
|
+
if (typeof maybe.path !== "string" || typeof maybe.content !== "string") {
|
|
2400
|
+
return undefined;
|
|
2401
|
+
}
|
|
2402
|
+
const relativePath = normalizeWorkspaceRelativePath(workspacePath, maybe.path, "throw");
|
|
2403
|
+
const action = maybe.action === "create" ? "create" : "update";
|
|
2404
|
+
const content = maybe.content;
|
|
2405
|
+
if (!relativePath || isSensitiveWorkspacePath(relativePath) || content.includes("\u0000")) {
|
|
2406
|
+
return undefined;
|
|
2407
|
+
}
|
|
2408
|
+
if (Buffer.byteLength(content, "utf8") > maxPatchProposalBytesPerFile) {
|
|
2409
|
+
throw new Error(`AI patch proposal for ${relativePath} is too large.`);
|
|
2410
|
+
}
|
|
2411
|
+
return {
|
|
2412
|
+
path: relativePath,
|
|
2413
|
+
action,
|
|
2414
|
+
content
|
|
2415
|
+
};
|
|
2416
|
+
})
|
|
2417
|
+
.filter((file) => Boolean(file));
|
|
2418
|
+
if (files.length === 0) {
|
|
2419
|
+
throw new Error(`${getModelProviderDisplayName(provider)} did not propose any safe file changes.`);
|
|
2420
|
+
}
|
|
2421
|
+
return {
|
|
2422
|
+
id: `patch_${createHash("sha256").update(`${session.session.id}:${provider}:${createdAt}`).digest("hex").slice(0, 12)}`,
|
|
2423
|
+
createdAt,
|
|
2424
|
+
provider,
|
|
2425
|
+
summary: typeof parsed.summary === "string" && parsed.summary.trim() ? parsed.summary.trim() : `AI patch proposal for ${session.workItem.key}`,
|
|
2426
|
+
files,
|
|
2427
|
+
risks: stringArrayOr(parsed.risks, [])
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
function normalizeWorkspaceRelativePath(workspacePath, requestedPath, mode) {
|
|
2431
|
+
const trimmed = requestedPath.trim();
|
|
2432
|
+
if (!trimmed) {
|
|
2433
|
+
return undefined;
|
|
2434
|
+
}
|
|
2435
|
+
const absolutePath = isAbsolute(trimmed) ? resolve(trimmed) : resolve(workspacePath, trimmed);
|
|
2436
|
+
const relativePath = relative(workspacePath, absolutePath).replace(/\\/gu, "/");
|
|
2437
|
+
const invalid = relativePath === "" || relativePath.startsWith("../") || relativePath === ".." || isAbsolute(relativePath);
|
|
2438
|
+
if (invalid) {
|
|
2439
|
+
if (mode === "throw") {
|
|
2440
|
+
throw new Error(`AI patch path is outside the workspace: ${requestedPath}`);
|
|
2441
|
+
}
|
|
2442
|
+
return undefined;
|
|
2443
|
+
}
|
|
2444
|
+
return relativePath;
|
|
2445
|
+
}
|
|
2446
|
+
function isSensitiveWorkspacePath(filePath) {
|
|
2447
|
+
const lower = filePath.toLowerCase();
|
|
2448
|
+
if (lower.includes("/.git/") || lower.startsWith(".git/") || lower.includes("/node_modules/") || lower.startsWith("node_modules/")) {
|
|
2449
|
+
return true;
|
|
2450
|
+
}
|
|
2451
|
+
return sensitivePathFragments.some((fragment) => lower === fragment || lower.includes(`/${fragment}`) || lower.endsWith(fragment));
|
|
2452
|
+
}
|
|
2453
|
+
async function applyPatchFiles(workspacePath, files) {
|
|
2454
|
+
for (const file of files) {
|
|
2455
|
+
const relativePath = normalizeWorkspaceRelativePath(workspacePath, file.path, "throw");
|
|
2456
|
+
if (!relativePath || isSensitiveWorkspacePath(relativePath)) {
|
|
2457
|
+
throw new Error(`Refusing to write unsafe AI patch path: ${file.path}`);
|
|
2458
|
+
}
|
|
2459
|
+
const absolutePath = resolve(workspacePath, relativePath);
|
|
2460
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
2461
|
+
await writeFile(absolutePath, file.content, "utf8");
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
1941
2464
|
async function discoverTestCommandCandidates(workspacePath) {
|
|
1942
2465
|
const scripts = await readPackageScripts(join(workspacePath, "package.json"));
|
|
1943
2466
|
const packageManager = detectPackageManager(workspacePath);
|
|
@@ -2028,7 +2551,24 @@ function createCommandApproval(session, candidate, now) {
|
|
|
2028
2551
|
status: "approved"
|
|
2029
2552
|
};
|
|
2030
2553
|
}
|
|
2031
|
-
function
|
|
2554
|
+
function createFileEditApproval(session, proposal, now, reason) {
|
|
2555
|
+
return {
|
|
2556
|
+
id: `approval_${createHash("sha256").update(`${session.session.id}:edit_files:${proposal.id}`).digest("hex").slice(0, 12)}`,
|
|
2557
|
+
type: "edit_files",
|
|
2558
|
+
title: `File edit approval for ${session.workItem.key}`,
|
|
2559
|
+
reason,
|
|
2560
|
+
details: [
|
|
2561
|
+
`Session: ${session.session.id}`,
|
|
2562
|
+
`Work item: ${session.workItem.key}`,
|
|
2563
|
+
`Workspace: ${session.workspaceCandidate?.workspace.name ?? "unresolved"}`,
|
|
2564
|
+
`Provider: ${getModelProviderDisplayName(proposal.provider)}`,
|
|
2565
|
+
`Files: ${proposal.files.map((file) => `${file.action} ${file.path}`).join(", ")}`,
|
|
2566
|
+
`Recorded at: ${now}`
|
|
2567
|
+
],
|
|
2568
|
+
status: "pending"
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
function buildPullRequestDraft(session, createdAt, baseBranch = "main") {
|
|
2032
2572
|
const workItem = session.workItem;
|
|
2033
2573
|
const workspace = session.workspaceCandidate?.workspace;
|
|
2034
2574
|
const title = `${workItem.key}: ${workItem.title}`;
|
|
@@ -2052,12 +2592,69 @@ function buildPullRequestDraft(session, createdAt) {
|
|
|
2052
2592
|
return {
|
|
2053
2593
|
title,
|
|
2054
2594
|
body,
|
|
2055
|
-
baseBranch
|
|
2056
|
-
headBranch: session
|
|
2595
|
+
baseBranch,
|
|
2596
|
+
headBranch: selectPullRequestBranchName(session),
|
|
2057
2597
|
remoteUrl: workspace?.remoteUrls[0],
|
|
2058
2598
|
createdAt
|
|
2059
2599
|
};
|
|
2060
2600
|
}
|
|
2601
|
+
function selectPullRequestBranchName(session) {
|
|
2602
|
+
const currentBranch = session.session.branchName?.trim();
|
|
2603
|
+
if (currentBranch && !["main", "master", "develop", "development"].includes(currentBranch)) {
|
|
2604
|
+
return currentBranch;
|
|
2605
|
+
}
|
|
2606
|
+
const slug = session.workItem.title
|
|
2607
|
+
.toLowerCase()
|
|
2608
|
+
.replace(/[^a-z0-9]+/gu, "-")
|
|
2609
|
+
.replace(/^-|-$/gu, "")
|
|
2610
|
+
.slice(0, 48);
|
|
2611
|
+
return `openpome/${session.workItem.key.toLowerCase()}${slug ? `-${slug}` : ""}`;
|
|
2612
|
+
}
|
|
2613
|
+
async function ensurePullRequestBranch(workspacePath, branch) {
|
|
2614
|
+
const currentBranch = (await runGit(workspacePath, ["branch", "--show-current"])).trim();
|
|
2615
|
+
if (currentBranch !== branch) {
|
|
2616
|
+
await runGitStrict(workspacePath, ["checkout", "-B", branch]);
|
|
2617
|
+
}
|
|
2618
|
+
return branch;
|
|
2619
|
+
}
|
|
2620
|
+
async function hasWorkspaceChanges(workspacePath) {
|
|
2621
|
+
const output = await runGit(workspacePath, ["status", "--porcelain"]);
|
|
2622
|
+
return output.trim().length > 0;
|
|
2623
|
+
}
|
|
2624
|
+
async function detectPullRequestBaseBranch(workspacePath) {
|
|
2625
|
+
const originHead = (await runGit(workspacePath, ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"])).trim();
|
|
2626
|
+
if (originHead.startsWith("origin/")) {
|
|
2627
|
+
return originHead.slice("origin/".length);
|
|
2628
|
+
}
|
|
2629
|
+
const remoteDefault = await runGit(workspacePath, ["remote", "show", "origin"]);
|
|
2630
|
+
const headLine = remoteDefault
|
|
2631
|
+
.split(/\r?\n/u)
|
|
2632
|
+
.map((line) => line.trim())
|
|
2633
|
+
.find((line) => line.startsWith("HEAD branch:"));
|
|
2634
|
+
const headBranch = headLine?.replace("HEAD branch:", "").trim();
|
|
2635
|
+
return headBranch || "main";
|
|
2636
|
+
}
|
|
2637
|
+
function hasPassedTestEvidence(session) {
|
|
2638
|
+
return (session.testRunEvidence ?? []).some((run) => run.status === "passed");
|
|
2639
|
+
}
|
|
2640
|
+
async function runGitStrict(cwd, args) {
|
|
2641
|
+
return execFileStrict("git", args, cwd);
|
|
2642
|
+
}
|
|
2643
|
+
async function execFileStrict(command, args, cwd) {
|
|
2644
|
+
try {
|
|
2645
|
+
const result = await execFileAsync(command, args, {
|
|
2646
|
+
cwd,
|
|
2647
|
+
timeout: 2 * 60 * 1000,
|
|
2648
|
+
maxBuffer: 1024 * 1024,
|
|
2649
|
+
windowsHide: true
|
|
2650
|
+
});
|
|
2651
|
+
return result.stdout;
|
|
2652
|
+
}
|
|
2653
|
+
catch (error) {
|
|
2654
|
+
const detail = summarizeExecError(error);
|
|
2655
|
+
throw new Error(`${command} ${args.join(" ")} failed${detail ? `: ${detail}` : "."}`);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2061
2658
|
function buildWorkItemUpdateDraft(session, createdAt) {
|
|
2062
2659
|
const lines = [
|
|
2063
2660
|
`OpenPome update for ${session.workItem.key}`,
|
|
@@ -2257,21 +2854,22 @@ function summarizeExecError(error) {
|
|
|
2257
2854
|
const message = typeof maybeError.message === "string" ? maybeError.message : "";
|
|
2258
2855
|
return stderr || stdout || message || undefined;
|
|
2259
2856
|
}
|
|
2260
|
-
|
|
2261
|
-
const paths = getOpenPomePaths();
|
|
2262
|
-
const persisted = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
2857
|
+
function createExternalActionApproval(session, type, now, details) {
|
|
2263
2858
|
return {
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
session:
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
:
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2859
|
+
id: `approval_${createHash("sha256").update(`${session.session.id}:${type}:${now}`).digest("hex").slice(0, 12)}`,
|
|
2860
|
+
type,
|
|
2861
|
+
title: type === "create_pr" ? `PR creation approval for ${session.workItem.key}` : `Work item update approval for ${session.workItem.key}`,
|
|
2862
|
+
reason: type === "create_pr"
|
|
2863
|
+
? "Developer explicitly requested OpenPome to create the GitHub pull request."
|
|
2864
|
+
: "Developer explicitly requested OpenPome to post the work item update.",
|
|
2865
|
+
details: [
|
|
2866
|
+
`Session: ${session.session.id}`,
|
|
2867
|
+
`Work item: ${session.workItem.key}`,
|
|
2868
|
+
`Workspace: ${session.workspaceCandidate?.workspace.name ?? "unresolved"}`,
|
|
2869
|
+
...details,
|
|
2870
|
+
`Recorded at: ${now}`
|
|
2871
|
+
],
|
|
2872
|
+
status: "approved"
|
|
2275
2873
|
};
|
|
2276
2874
|
}
|
|
2277
2875
|
function createPlanApproval(session, status, now, reason = "Developer reviewed the implementation plan.") {
|