@openpome/local-gateway 0.33.0-alpha.0 → 0.37.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +578 -42
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ const maxWorkspaceScanRepositories = 200;
|
|
|
40
40
|
export function getGatewayHealth() {
|
|
41
41
|
return {
|
|
42
42
|
status: "ok",
|
|
43
|
-
version: "0.
|
|
43
|
+
version: "0.37.0-alpha.0"
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
export async function initOpenPome() {
|
|
@@ -231,6 +231,11 @@ export async function runDoctor(env = process.env) {
|
|
|
231
231
|
name: "Model provider",
|
|
232
232
|
status: activeModel?.configured ? "ok" : "attention",
|
|
233
233
|
detail: activeModel?.detail ?? "Run `pome auth ai status` to inspect AI setup."
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "Telemetry",
|
|
237
|
+
status: "ok",
|
|
238
|
+
detail: "Disabled by default. OpenPome does not send analytics, prompts, source code, diffs, or crash data."
|
|
234
239
|
}
|
|
235
240
|
];
|
|
236
241
|
return {
|
|
@@ -493,6 +498,99 @@ export async function getTaskSessionStatus() {
|
|
|
493
498
|
aiPatchProposal: persisted.aiPatchProposal
|
|
494
499
|
};
|
|
495
500
|
}
|
|
501
|
+
export async function getAssistantDecision() {
|
|
502
|
+
const status = await getTaskSessionStatus();
|
|
503
|
+
if (!status.active || !status.session || !status.workItem) {
|
|
504
|
+
const jira = await getJiraAuthStatus();
|
|
505
|
+
if (!jira.configured) {
|
|
506
|
+
return buildAssistantDecision(status, "connect_jira", "Connect Jira", "OpenPome needs Jira access before it can show assigned stories.", [
|
|
507
|
+
"pome onboard",
|
|
508
|
+
"pome auth jira login --listen",
|
|
509
|
+
"pome demo"
|
|
510
|
+
], [jira.detail]);
|
|
511
|
+
}
|
|
512
|
+
return buildAssistantDecision(status, "select_work", "Choose assigned work", "Fetch assigned work and start one story.", [
|
|
513
|
+
"pome work",
|
|
514
|
+
"pome start <KEY>"
|
|
515
|
+
]);
|
|
516
|
+
}
|
|
517
|
+
if (!status.plan) {
|
|
518
|
+
return buildAssistantDecision(status, "create_plan", "Create implementation plan", "Build a repo-aware implementation plan from the latest Jira story.", [
|
|
519
|
+
"pome plan"
|
|
520
|
+
]);
|
|
521
|
+
}
|
|
522
|
+
if (status.planApproval?.status !== "approved") {
|
|
523
|
+
return buildAssistantDecision(status, "approve_plan", "Approve the plan", "Review the implementation plan before OpenPome asks AI for file changes.", [
|
|
524
|
+
"pome approve"
|
|
525
|
+
], collectPlanReadinessWarnings(status));
|
|
526
|
+
}
|
|
527
|
+
if (status.aiPatchProposal && !status.aiPatchProposal.appliedAt) {
|
|
528
|
+
return buildAssistantDecision(status, "approve_patch", "Approve AI patch", "Review the proposed file changes. OpenPome writes files only after approval.", [
|
|
529
|
+
"pome approve"
|
|
530
|
+
]);
|
|
531
|
+
}
|
|
532
|
+
const latestPatchAppliedAt = status.aiPatchProposal?.appliedAt;
|
|
533
|
+
const latestRunAfterPatch = getLatestTestRunAfterStatus(status, latestPatchAppliedAt);
|
|
534
|
+
if (latestRunAfterPatch?.status === "failed") {
|
|
535
|
+
return buildAssistantDecision(status, "retry_failed_tests", "Repair failed validation", "The latest approved test failed. Ask AI for a focused repair patch using the failed test evidence.", [
|
|
536
|
+
"pome next"
|
|
537
|
+
]);
|
|
538
|
+
}
|
|
539
|
+
if (!status.aiPatchProposal && !status.diffSummary) {
|
|
540
|
+
const model = await getModelProviderStatus();
|
|
541
|
+
const activeModel = model.providers.find((provider) => provider.active);
|
|
542
|
+
const aiCanProposePatch = activeModel?.provider !== "manual-copy" && Boolean(activeModel?.configured);
|
|
543
|
+
const blockers = aiCanProposePatch ? collectPlanReadinessWarnings(status) : [
|
|
544
|
+
activeModel?.detail ?? "No AI provider is active.",
|
|
545
|
+
"Connect Claude CLI, Claude API, or OpenAI before AI patch proposals.",
|
|
546
|
+
"Run `pome auth ai claude-cli`, `pome auth ai claude`, or `pome auth ai openai`."
|
|
547
|
+
];
|
|
548
|
+
return buildAssistantDecision(status, "propose_patch", "Ask AI for the smallest safe patch", "OpenPome will collect bounded repo context, ask the active AI provider for changes, and prepare an approval checkpoint.", [
|
|
549
|
+
"pome next",
|
|
550
|
+
"pome auth ai claude-cli"
|
|
551
|
+
], blockers);
|
|
552
|
+
}
|
|
553
|
+
if (status.aiPatchProposal?.appliedAt && (status.testCommandCandidates?.length ?? 0) === 0) {
|
|
554
|
+
return buildAssistantDecision(status, "discover_tests", "Discover validation commands", "Find the likely test or validation commands for this workspace.", [
|
|
555
|
+
"pome next"
|
|
556
|
+
]);
|
|
557
|
+
}
|
|
558
|
+
if ((status.testCommandCandidates?.length ?? 0) > 0 && (status.commandApprovalEvidence?.length ?? 0) === 0) {
|
|
559
|
+
return buildAssistantDecision(status, "approve_test", "Approve one validation command", "OpenPome will not run commands until you approve one candidate.", [
|
|
560
|
+
"pome approve"
|
|
561
|
+
]);
|
|
562
|
+
}
|
|
563
|
+
if (status.aiPatchProposal?.appliedAt && (status.commandApprovalEvidence?.length ?? 0) > 0 && !latestRunAfterPatch) {
|
|
564
|
+
return buildAssistantDecision(status, "run_tests", "Run approved validation", "Run the approved command and store bounded evidence for PR and Jira updates.", [
|
|
565
|
+
"pome next"
|
|
566
|
+
]);
|
|
567
|
+
}
|
|
568
|
+
if (!status.prDraft || !status.workItemUpdateDraft) {
|
|
569
|
+
return buildAssistantDecision(status, "prepare_completion", "Prepare completion drafts", "Prepare the PR body and Jira update from the approved plan, diff summary, and validation evidence.", [
|
|
570
|
+
"pome done"
|
|
571
|
+
]);
|
|
572
|
+
}
|
|
573
|
+
if (!status.prCreation) {
|
|
574
|
+
const github = await getGitHubAuthStatus();
|
|
575
|
+
if (!github.authenticated) {
|
|
576
|
+
return buildAssistantDecision(status, "connect_github", "Connect GitHub", "GitHub is needed before OpenPome can create the pull request.", [
|
|
577
|
+
"pome auth github login",
|
|
578
|
+
"pome pr create"
|
|
579
|
+
], [github.detail]);
|
|
580
|
+
}
|
|
581
|
+
return buildAssistantDecision(status, "create_pr", "Create the pull request", "OpenPome will create the branch/commit/push and open the PR through native GitHub API or GitHub CLI fallback.", [
|
|
582
|
+
"pome pr create"
|
|
583
|
+
]);
|
|
584
|
+
}
|
|
585
|
+
if (!status.workItemUpdatePost) {
|
|
586
|
+
return buildAssistantDecision(status, "post_work_update", "Post Jira update", "Post the prepared Jira update with PR and validation context.", [
|
|
587
|
+
"pome work-item post-update"
|
|
588
|
+
]);
|
|
589
|
+
}
|
|
590
|
+
return buildAssistantDecision(status, "complete", "Story handoff ready", "External completion artifacts are created. Review Jira and GitHub for your team's final workflow.", [
|
|
591
|
+
"pome status"
|
|
592
|
+
]);
|
|
593
|
+
}
|
|
496
594
|
export async function getTaskSessionTimeline() {
|
|
497
595
|
const paths = getOpenPomePaths();
|
|
498
596
|
const persisted = await refreshActiveTaskSessionWorkItem(paths.homeDirectory);
|
|
@@ -513,6 +611,34 @@ export async function getTaskSessionApprovalHistory() {
|
|
|
513
611
|
approvals: persisted?.approvalHistory ?? []
|
|
514
612
|
};
|
|
515
613
|
}
|
|
614
|
+
function buildAssistantDecision(status, action, title, detail, commands, blockers = []) {
|
|
615
|
+
return {
|
|
616
|
+
action,
|
|
617
|
+
title,
|
|
618
|
+
detail,
|
|
619
|
+
commands,
|
|
620
|
+
blockers: blockers.filter((blocker) => blocker.trim().length > 0),
|
|
621
|
+
status
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function collectPlanReadinessWarnings(status) {
|
|
625
|
+
const warnings = [];
|
|
626
|
+
if (!status.workspaceCandidate?.workspace.path) {
|
|
627
|
+
warnings.push("No workspace is resolved yet. Start from inside the repo or link the work item to a workspace.");
|
|
628
|
+
}
|
|
629
|
+
else if (status.workspaceCandidate.confidence < 0.5) {
|
|
630
|
+
warnings.push("Workspace confidence is low. OpenPome can continue, but confirm the repo if the plan looks wrong.");
|
|
631
|
+
}
|
|
632
|
+
if (status.plan?.missingInfo.length) {
|
|
633
|
+
warnings.push(...status.plan.missingInfo.map((item) => `Missing context: ${item}`));
|
|
634
|
+
}
|
|
635
|
+
return warnings;
|
|
636
|
+
}
|
|
637
|
+
function getLatestTestRunAfterStatus(status, since) {
|
|
638
|
+
const runs = status.testRunEvidence ?? [];
|
|
639
|
+
const filtered = since ? runs.filter((run) => run.finishedAt >= since) : runs;
|
|
640
|
+
return filtered[filtered.length - 1];
|
|
641
|
+
}
|
|
516
642
|
export async function stopTaskSession() {
|
|
517
643
|
const paths = getOpenPomePaths();
|
|
518
644
|
const persisted = await readActiveTaskSessionIfPresent(paths.homeDirectory);
|
|
@@ -934,7 +1060,7 @@ export async function discoverTestCommands() {
|
|
|
934
1060
|
}
|
|
935
1061
|
const workspace = persisted.workspaceCandidate?.workspace;
|
|
936
1062
|
const discoveredAt = new Date().toISOString();
|
|
937
|
-
const candidates = workspace?.path ? await discoverTestCommandCandidates(workspace.path) : getFallbackTestCommandCandidates();
|
|
1063
|
+
const candidates = workspace?.path ? await discoverTestCommandCandidates(workspace.path, persisted) : getFallbackTestCommandCandidates();
|
|
938
1064
|
await writeActiveTaskSession(paths.homeDirectory, {
|
|
939
1065
|
...persisted,
|
|
940
1066
|
testCommandCandidates: candidates,
|
|
@@ -964,7 +1090,7 @@ export async function approveTestCommand(command) {
|
|
|
964
1090
|
const candidates = persisted.testCommandCandidates?.length
|
|
965
1091
|
? persisted.testCommandCandidates
|
|
966
1092
|
: persisted.workspaceCandidate?.workspace.path
|
|
967
|
-
? await discoverTestCommandCandidates(persisted.workspaceCandidate.workspace.path)
|
|
1093
|
+
? await discoverTestCommandCandidates(persisted.workspaceCandidate.workspace.path, persisted)
|
|
968
1094
|
: getFallbackTestCommandCandidates();
|
|
969
1095
|
const selected = selectTestCommandCandidate(candidates, command);
|
|
970
1096
|
if (!selected) {
|
|
@@ -1192,16 +1318,16 @@ export async function createGitHubDeviceLogin(env = process.env) {
|
|
|
1192
1318
|
client_id: clientId,
|
|
1193
1319
|
scope
|
|
1194
1320
|
});
|
|
1195
|
-
const response = await
|
|
1321
|
+
const response = await fetchGitHub("https://github.com/login/device/code", {
|
|
1196
1322
|
method: "POST",
|
|
1197
1323
|
headers: {
|
|
1198
1324
|
Accept: "application/json",
|
|
1199
1325
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
1200
1326
|
},
|
|
1201
1327
|
body
|
|
1202
|
-
});
|
|
1328
|
+
}, "start browser login");
|
|
1203
1329
|
if (!response.ok) {
|
|
1204
|
-
throw new Error(
|
|
1330
|
+
throw new Error(await getGitHubStatusGuidance(response, "start browser login"));
|
|
1205
1331
|
}
|
|
1206
1332
|
const payload = (await response.json());
|
|
1207
1333
|
if (!payload.device_code || !payload.user_code || !payload.verification_uri) {
|
|
@@ -1365,7 +1491,7 @@ export async function createPullRequest(options = {}) {
|
|
|
1365
1491
|
}
|
|
1366
1492
|
await runGitStrict(workspacePath, ["add", "-A"]);
|
|
1367
1493
|
await runGitStrict(workspacePath, ["commit", "-m", commitMessage]);
|
|
1368
|
-
await
|
|
1494
|
+
await pushPullRequestBranch(workspacePath, branch);
|
|
1369
1495
|
const storedGitHubToken = github.tokenSource === "openpome" ? await readStoredGitHubOAuth() : undefined;
|
|
1370
1496
|
const prProvider = storedGitHubToken?.accessToken ? "github-api" : "github-cli";
|
|
1371
1497
|
const prUrl = storedGitHubToken?.accessToken
|
|
@@ -1756,15 +1882,15 @@ async function isGitHubCliAuthenticated() {
|
|
|
1756
1882
|
}
|
|
1757
1883
|
}
|
|
1758
1884
|
async function fetchGitHubAuthenticatedUser(accessToken) {
|
|
1759
|
-
const response = await
|
|
1885
|
+
const response = await fetchGitHub("https://api.github.com/user", {
|
|
1760
1886
|
headers: {
|
|
1761
1887
|
Accept: "application/vnd.github+json",
|
|
1762
1888
|
Authorization: `Bearer ${accessToken}`,
|
|
1763
1889
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
1764
1890
|
}
|
|
1765
|
-
});
|
|
1891
|
+
}, "verify authenticated user");
|
|
1766
1892
|
if (!response.ok) {
|
|
1767
|
-
throw new Error(
|
|
1893
|
+
throw new Error(await getGitHubStatusGuidance(response, "verify authenticated user"));
|
|
1768
1894
|
}
|
|
1769
1895
|
const payload = (await response.json());
|
|
1770
1896
|
if (!payload.login) {
|
|
@@ -1777,7 +1903,7 @@ async function fetchGitHubAuthenticatedUser(accessToken) {
|
|
|
1777
1903
|
};
|
|
1778
1904
|
}
|
|
1779
1905
|
async function pollGitHubDeviceAccessToken(clientId, deviceCode) {
|
|
1780
|
-
const response = await
|
|
1906
|
+
const response = await fetchGitHub("https://github.com/login/oauth/access_token", {
|
|
1781
1907
|
method: "POST",
|
|
1782
1908
|
headers: {
|
|
1783
1909
|
Accept: "application/json",
|
|
@@ -1788,11 +1914,11 @@ async function pollGitHubDeviceAccessToken(clientId, deviceCode) {
|
|
|
1788
1914
|
device_code: deviceCode,
|
|
1789
1915
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
1790
1916
|
})
|
|
1791
|
-
});
|
|
1917
|
+
}, "complete browser login");
|
|
1792
1918
|
if (!response.ok) {
|
|
1793
1919
|
return {
|
|
1794
1920
|
status: "error",
|
|
1795
|
-
detail:
|
|
1921
|
+
detail: await getGitHubStatusGuidance(response, "complete browser login")
|
|
1796
1922
|
};
|
|
1797
1923
|
}
|
|
1798
1924
|
const payload = (await response.json());
|
|
@@ -1830,6 +1956,48 @@ function parseGitHubScopes(scope) {
|
|
|
1830
1956
|
.map((value) => value.trim())
|
|
1831
1957
|
.filter((value) => value.length > 0);
|
|
1832
1958
|
}
|
|
1959
|
+
async function fetchGitHub(input, init, action) {
|
|
1960
|
+
try {
|
|
1961
|
+
return await fetch(input, init);
|
|
1962
|
+
}
|
|
1963
|
+
catch (error) {
|
|
1964
|
+
throw new Error(getGitHubNetworkGuidance(action, error));
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
async function getGitHubStatusGuidance(response, action) {
|
|
1968
|
+
const body = await safeResponseText(response);
|
|
1969
|
+
const detail = body ? ` Detail: ${summarizeProviderBody(body)}` : "";
|
|
1970
|
+
if (response.status === 401) {
|
|
1971
|
+
return `GitHub ${action} was unauthorized (401). Run \`pome auth github login\` again, or \`gh auth login\` if you use the GitHub CLI fallback.${detail}`;
|
|
1972
|
+
}
|
|
1973
|
+
if (response.status === 403) {
|
|
1974
|
+
return `GitHub ${action} was forbidden (403). Check repository permission, organization SSO, token scopes, branch protection, and whether the token has \`repo\` access.${detail}`;
|
|
1975
|
+
}
|
|
1976
|
+
if (response.status === 404) {
|
|
1977
|
+
return `GitHub ${action} could not find the repository or resource (404). Check the git remote, repository visibility, GitHub Enterprise host, and account access.${detail}`;
|
|
1978
|
+
}
|
|
1979
|
+
if (response.status === 422) {
|
|
1980
|
+
return `GitHub ${action} was rejected (422). Check whether the branch already has an open PR, base/head branch names are valid, and the repo allows PRs.${detail}`;
|
|
1981
|
+
}
|
|
1982
|
+
if (response.status === 429 || response.headers.get("x-ratelimit-remaining") === "0") {
|
|
1983
|
+
const reset = response.headers.get("x-ratelimit-reset");
|
|
1984
|
+
const resetDetail = reset ? ` Rate limit resets at ${new Date(Number(reset) * 1000).toISOString()}.` : "";
|
|
1985
|
+
return `GitHub rate limit reached while trying to ${action}.${resetDetail} Wait and retry, or use a token with the right organization access.${detail}`;
|
|
1986
|
+
}
|
|
1987
|
+
if (response.status >= 500) {
|
|
1988
|
+
return `GitHub ${action} failed with ${response.status} ${response.statusText}. GitHub may be unavailable, blocked by a proxy, or unreachable from this network.${detail}`;
|
|
1989
|
+
}
|
|
1990
|
+
return `GitHub ${action} failed: ${response.status} ${response.statusText}.${detail}`;
|
|
1991
|
+
}
|
|
1992
|
+
function getGitHubNetworkGuidance(action, error) {
|
|
1993
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1994
|
+
return [
|
|
1995
|
+
`GitHub ${action} could not reach GitHub.`,
|
|
1996
|
+
"Check internet access, VPN split tunneling, proxy/firewall rules, corporate certificate trust, and GitHub Enterprise host configuration.",
|
|
1997
|
+
"Run `pome auth github status` after fixing network access.",
|
|
1998
|
+
`Detail: ${detail}`
|
|
1999
|
+
].join(" ");
|
|
2000
|
+
}
|
|
1833
2001
|
function summarizeUnknownError(error) {
|
|
1834
2002
|
return error instanceof Error ? error.message : String(error);
|
|
1835
2003
|
}
|
|
@@ -1878,7 +2046,11 @@ function getOpenPomePaths() {
|
|
|
1878
2046
|
async function readConfigIfPresent(configFile) {
|
|
1879
2047
|
try {
|
|
1880
2048
|
const content = await readFile(configFile, "utf8");
|
|
1881
|
-
return
|
|
2049
|
+
return {
|
|
2050
|
+
...defaultConfig,
|
|
2051
|
+
...JSON.parse(content),
|
|
2052
|
+
telemetryEnabled: false
|
|
2053
|
+
};
|
|
1882
2054
|
}
|
|
1883
2055
|
catch (error) {
|
|
1884
2056
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -2461,22 +2633,40 @@ function selectArchivedTaskSession(sessions, sessionId) {
|
|
|
2461
2633
|
}
|
|
2462
2634
|
function buildPlanningContext(session) {
|
|
2463
2635
|
const workspace = session.workspaceCandidate?.workspace;
|
|
2636
|
+
const missingRequirementSignals = detectMissingRequirementSignals(session.workItem);
|
|
2464
2637
|
const context = [
|
|
2465
2638
|
`Work item type: ${session.workItem.type}`,
|
|
2466
2639
|
`Status: ${session.workItem.status}`,
|
|
2467
2640
|
session.workItem.priority ? `Priority: ${session.workItem.priority}` : undefined,
|
|
2641
|
+
session.workItem.description ? `Description length: ${session.workItem.description.length} characters` : "Description: not provided",
|
|
2642
|
+
hasExplicitAcceptanceCriteria(session.workItem)
|
|
2643
|
+
? "Acceptance criteria: detected in work item text"
|
|
2644
|
+
: "Acceptance criteria: not explicit; identify missing acceptance criteria before implementation",
|
|
2645
|
+
missingRequirementSignals.length ? `Missing requirement signals: ${missingRequirementSignals.join("; ")}` : undefined,
|
|
2468
2646
|
session.workItem.labels?.length ? `Labels: ${session.workItem.labels.join(", ")}` : undefined,
|
|
2469
2647
|
session.workItem.components?.length ? `Components: ${session.workItem.components.join(", ")}` : undefined,
|
|
2648
|
+
session.workItem.links?.length ? `Linked references: ${session.workItem.links.map((link) => `${link.kind}:${link.title ?? link.url}`).join("; ")}` : undefined,
|
|
2649
|
+
session.workItem.subtasks?.length ? `Subtasks: ${session.workItem.subtasks.map((subtask) => `${subtask.key} ${subtask.status} ${subtask.title}`).join("; ")}` : undefined,
|
|
2470
2650
|
workspace ? `Workspace: ${workspace.name}` : "Workspace: unresolved",
|
|
2471
2651
|
workspace?.path ? `Workspace path: ${workspace.path}` : undefined,
|
|
2472
2652
|
session.workspaceCandidate ? `Workspace confidence: ${Math.round(session.workspaceCandidate.confidence * 100)}%` : undefined,
|
|
2473
|
-
session.workspaceCandidate?.reasons.length ? `Workspace reasons: ${session.workspaceCandidate.reasons.join("; ")}` : undefined
|
|
2653
|
+
session.workspaceCandidate?.reasons.length ? `Workspace reasons: ${session.workspaceCandidate.reasons.join("; ")}` : undefined,
|
|
2654
|
+
workspace?.packageNames?.length ? `Workspace packages: ${workspace.packageNames.slice(0, 8).join(", ")}` : undefined,
|
|
2655
|
+
workspace?.readmeKeywords?.length ? `README signals: ${workspace.readmeKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
2656
|
+
workspace?.codeownersKeywords?.length ? `Ownership signals: ${workspace.codeownersKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
2657
|
+
workspace?.recentBranches?.length ? `Recent branches: ${workspace.recentBranches.slice(0, 8).join(", ")}` : undefined,
|
|
2658
|
+
workspace?.recentCommitRefs?.length ? `Recent work refs: ${workspace.recentCommitRefs.slice(0, 12).join(", ")}` : undefined
|
|
2474
2659
|
];
|
|
2475
2660
|
return context.filter((item) => Boolean(item));
|
|
2476
2661
|
}
|
|
2477
2662
|
function buildInitialImplementationPlan(workItem, workspaceCandidate) {
|
|
2478
2663
|
const workspace = workspaceCandidate?.workspace;
|
|
2479
2664
|
const hasWorkspace = Boolean(workspace?.path);
|
|
2665
|
+
const missingRequirementSignals = detectMissingRequirementSignals(workItem);
|
|
2666
|
+
const missingInfo = [
|
|
2667
|
+
hasWorkspace ? undefined : "No workspace candidate is selected yet.",
|
|
2668
|
+
...missingRequirementSignals
|
|
2669
|
+
].filter((item) => Boolean(item));
|
|
2480
2670
|
return {
|
|
2481
2671
|
summary: `Prepare implementation for ${workItem.key}: ${workItem.title}`,
|
|
2482
2672
|
assumptions: [
|
|
@@ -2516,9 +2706,42 @@ function buildInitialImplementationPlan(workItem, workspaceCandidate) {
|
|
|
2516
2706
|
"Workspace resolution may be incomplete until real GitHub and historical session signals are added.",
|
|
2517
2707
|
"Manual-copy mode uses deterministic planning; connect OpenAI or Claude for AI-assisted planning and patch proposals."
|
|
2518
2708
|
],
|
|
2519
|
-
missingInfo
|
|
2709
|
+
missingInfo
|
|
2520
2710
|
};
|
|
2521
2711
|
}
|
|
2712
|
+
function hasExplicitAcceptanceCriteria(workItem) {
|
|
2713
|
+
const text = [workItem.title, workItem.description].filter(Boolean).join("\n").toLowerCase();
|
|
2714
|
+
return /\b(acceptance criteria|acceptance|criteria|given\b.*\bwhen\b.*\bthen|expected result|definition of done|done when|should|expected behavior|success criteria|verify|validation)\b/su.test(text);
|
|
2715
|
+
}
|
|
2716
|
+
function detectMissingRequirementSignals(workItem) {
|
|
2717
|
+
const text = [workItem.title, workItem.description].filter(Boolean).join("\n").trim();
|
|
2718
|
+
const lower = text.toLowerCase();
|
|
2719
|
+
const signals = [];
|
|
2720
|
+
if (!workItem.description || workItem.description.trim().length < 40) {
|
|
2721
|
+
signals.push("Work item description is short; confirm exact scope before broad edits.");
|
|
2722
|
+
}
|
|
2723
|
+
if (!hasExplicitAcceptanceCriteria(workItem)) {
|
|
2724
|
+
signals.push("Acceptance criteria are not explicit in the work item.");
|
|
2725
|
+
}
|
|
2726
|
+
if (workItem.type === "bug") {
|
|
2727
|
+
const hasExpected = /\b(expected|should happen|desired behavior|correct behavior)\b/u.test(lower);
|
|
2728
|
+
const hasActual = /\b(actual|currently|observed|happens now|error|failure|failed)\b/u.test(lower);
|
|
2729
|
+
const hasRepro = /\b(steps to reproduce|repro|reproduce|given\b.*\bwhen\b.*\bthen)\b/su.test(lower);
|
|
2730
|
+
if (!hasExpected || !hasActual) {
|
|
2731
|
+
signals.push("Bug report is missing clear expected vs actual behavior.");
|
|
2732
|
+
}
|
|
2733
|
+
if (!hasRepro) {
|
|
2734
|
+
signals.push("Bug report has no clear reproduction steps.");
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
if (!workItem.labels?.length && !workItem.components?.length) {
|
|
2738
|
+
signals.push("No labels or components are available to narrow the code area.");
|
|
2739
|
+
}
|
|
2740
|
+
if (!workItem.links?.some((link) => link.kind === "code" || link.kind === "pull_request" || link.kind === "document")) {
|
|
2741
|
+
signals.push("No linked code, pull request, or reference document is attached.");
|
|
2742
|
+
}
|
|
2743
|
+
return Array.from(new Set(signals)).slice(0, 6);
|
|
2744
|
+
}
|
|
2522
2745
|
async function buildImplementationPlan(persisted, prompt) {
|
|
2523
2746
|
const config = await readConfigIfPresent(getOpenPomePaths().configFile);
|
|
2524
2747
|
const provider = normalizeModelProviderId(config?.activeModelProvider ?? defaultConfig.activeModelProvider);
|
|
@@ -2544,7 +2767,7 @@ async function completeModelText(provider, prompt) {
|
|
|
2544
2767
|
return completeClaudeCliText(prompt);
|
|
2545
2768
|
}
|
|
2546
2769
|
async function completeOpenAIText(prompt, apiKey) {
|
|
2547
|
-
const response = await
|
|
2770
|
+
const response = await fetchModelProvider("OpenAI", "https://api.openai.com/v1/responses", {
|
|
2548
2771
|
method: "POST",
|
|
2549
2772
|
headers: {
|
|
2550
2773
|
"authorization": `Bearer ${apiKey}`,
|
|
@@ -2556,7 +2779,7 @@ async function completeOpenAIText(prompt, apiKey) {
|
|
|
2556
2779
|
})
|
|
2557
2780
|
});
|
|
2558
2781
|
if (!response.ok) {
|
|
2559
|
-
throw new Error(
|
|
2782
|
+
throw new Error(await getModelProviderStatusGuidance("OpenAI", response, "generate a plan or patch"));
|
|
2560
2783
|
}
|
|
2561
2784
|
const body = await response.json();
|
|
2562
2785
|
if (typeof body.output_text === "string") {
|
|
@@ -2570,7 +2793,7 @@ async function completeOpenAIText(prompt, apiKey) {
|
|
|
2570
2793
|
.join("\n");
|
|
2571
2794
|
}
|
|
2572
2795
|
async function completeAnthropicText(prompt, apiKey) {
|
|
2573
|
-
const response = await
|
|
2796
|
+
const response = await fetchModelProvider("Claude", "https://api.anthropic.com/v1/messages", {
|
|
2574
2797
|
method: "POST",
|
|
2575
2798
|
headers: {
|
|
2576
2799
|
"x-api-key": apiKey,
|
|
@@ -2589,7 +2812,7 @@ async function completeAnthropicText(prompt, apiKey) {
|
|
|
2589
2812
|
})
|
|
2590
2813
|
});
|
|
2591
2814
|
if (!response.ok) {
|
|
2592
|
-
throw new Error(
|
|
2815
|
+
throw new Error(await getModelProviderStatusGuidance("Claude", response, "generate a plan or patch"));
|
|
2593
2816
|
}
|
|
2594
2817
|
const body = await response.json();
|
|
2595
2818
|
const content = Array.isArray(body.content) ? body.content : [];
|
|
@@ -2631,12 +2854,50 @@ async function completeClaudeCliText(prompt) {
|
|
|
2631
2854
|
throw new Error(`Claude CLI request failed: ${summarizeExecError(error) || String(error)}`);
|
|
2632
2855
|
}
|
|
2633
2856
|
}
|
|
2857
|
+
async function fetchModelProvider(provider, input, init) {
|
|
2858
|
+
try {
|
|
2859
|
+
return await fetch(input, init);
|
|
2860
|
+
}
|
|
2861
|
+
catch (error) {
|
|
2862
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2863
|
+
throw new Error(`${provider} could not be reached. Check internet/VPN/proxy access, corporate certificate trust, and provider allowlists. Use \`pome auth ai status\` to verify setup. Detail: ${detail}`);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
async function getModelProviderStatusGuidance(provider, response, action) {
|
|
2867
|
+
const body = await safeResponseText(response);
|
|
2868
|
+
const detail = body ? ` Detail: ${summarizeProviderBody(body)}` : "";
|
|
2869
|
+
const authCommand = provider === "OpenAI" ? "pome auth ai openai" : "pome auth ai claude";
|
|
2870
|
+
if (response.status === 401) {
|
|
2871
|
+
return `${provider} ${action} was unauthorized (401). Reconnect with \`${authCommand}\` or verify the provider API key in your OS credential store/environment.${detail}`;
|
|
2872
|
+
}
|
|
2873
|
+
if (response.status === 403) {
|
|
2874
|
+
return `${provider} ${action} was forbidden (403). Check organization policy, model access, provider project permissions, and corporate egress rules.${detail}`;
|
|
2875
|
+
}
|
|
2876
|
+
if (response.status === 404) {
|
|
2877
|
+
return `${provider} ${action} could not find the configured model or endpoint (404). Check OPENPOME_${provider === "OpenAI" ? "OPENAI_MODEL" : "ANTHROPIC_MODEL"} and provider access.${detail}`;
|
|
2878
|
+
}
|
|
2879
|
+
if (response.status === 408 || response.status === 409 || response.status === 429) {
|
|
2880
|
+
return `${provider} is busy or rate limited (${response.status}). Wait and retry, or choose a smaller model/context. OpenPome has not written files.${detail}`;
|
|
2881
|
+
}
|
|
2882
|
+
if (response.status >= 500) {
|
|
2883
|
+
return `${provider} failed with ${response.status} ${response.statusText}. Provider service may be unavailable or blocked by your network/proxy. Retry later.${detail}`;
|
|
2884
|
+
}
|
|
2885
|
+
return `${provider} ${action} failed: ${response.status} ${response.statusText}.${detail}`;
|
|
2886
|
+
}
|
|
2634
2887
|
function buildStructuredPlanPrompt(prompt) {
|
|
2635
2888
|
return [
|
|
2636
2889
|
"You are OpenPome's planning engine.",
|
|
2890
|
+
"Plan like a senior developer assistant working from a live corporate work item.",
|
|
2891
|
+
"Prefer the smallest repo-aware change that satisfies the work item. Call out unclear scope instead of inventing requirements.",
|
|
2892
|
+
"Use workspace metadata, labels, linked references, ownership signals, and recent branch/commit refs to rank likely files.",
|
|
2893
|
+
"Suggest targeted validation commands before broad commands when the work item points to a specific component.",
|
|
2637
2894
|
"Return only compact JSON with this exact shape:",
|
|
2638
2895
|
"{\"summary\":\"...\",\"assumptions\":[\"...\"],\"steps\":[{\"id\":\"1\",\"title\":\"...\",\"detail\":\"...\"}],\"filesLikelyChanged\":[\"...\"],\"commandsToRun\":[\"...\"],\"risks\":[\"...\"],\"missingInfo\":[\"...\"]}",
|
|
2639
|
-
"
|
|
2896
|
+
"Rules:",
|
|
2897
|
+
"- Do not include source code, full diffs, secrets, or markdown fences.",
|
|
2898
|
+
"- Put missing acceptance criteria, missing repro steps, unclear expected behavior, and missing code links in missingInfo.",
|
|
2899
|
+
"- Keep filesLikelyChanged to relative paths or package/module hints when exact files are unknown.",
|
|
2900
|
+
"- Keep commandsToRun executable from the selected workspace.",
|
|
2640
2901
|
"",
|
|
2641
2902
|
prompt
|
|
2642
2903
|
].join("\n");
|
|
@@ -2695,39 +2956,79 @@ const modelProviderTimeoutMs = 120_000;
|
|
|
2695
2956
|
const modelProviderMaxBufferBytes = 2 * 1024 * 1024;
|
|
2696
2957
|
const sensitivePathFragments = [
|
|
2697
2958
|
".env",
|
|
2959
|
+
".env.local",
|
|
2960
|
+
".env.production",
|
|
2961
|
+
".env.development",
|
|
2698
2962
|
".npmrc",
|
|
2963
|
+
".yarnrc",
|
|
2964
|
+
".pnpmrc",
|
|
2699
2965
|
".pypirc",
|
|
2700
2966
|
".netrc",
|
|
2701
2967
|
".ssh",
|
|
2702
2968
|
".aws",
|
|
2703
2969
|
".gcp",
|
|
2704
2970
|
".azure",
|
|
2971
|
+
".kube",
|
|
2972
|
+
".docker",
|
|
2973
|
+
"credentials",
|
|
2974
|
+
"credential",
|
|
2975
|
+
"secrets",
|
|
2976
|
+
"secret",
|
|
2977
|
+
"private-key",
|
|
2978
|
+
"private_key",
|
|
2705
2979
|
"id_rsa",
|
|
2706
2980
|
"id_dsa",
|
|
2707
|
-
"id_ed25519"
|
|
2981
|
+
"id_ed25519",
|
|
2982
|
+
".pem",
|
|
2983
|
+
".key",
|
|
2984
|
+
".crt",
|
|
2985
|
+
".p12",
|
|
2986
|
+
".pfx"
|
|
2987
|
+
];
|
|
2988
|
+
const sensitiveContentPatterns = [
|
|
2989
|
+
/-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/u,
|
|
2990
|
+
/\b(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|OPENPOME_JIRA_API_TOKEN|OPENPOME_GITHUB_TOKEN|GITHUB_TOKEN|NPM_TOKEN)\s*=\s*['"]?[^'"\s]+/iu,
|
|
2991
|
+
/\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|private[_-]?key)\s*[:=]\s*['"][^'"]{12,}['"]/iu,
|
|
2992
|
+
/\b(?:ghp|github_pat|npm|sk|sk-ant|xox[baprs])-?[A-Za-z0-9_=-]{20,}\b/u
|
|
2708
2993
|
];
|
|
2709
2994
|
async function collectPatchContextFiles(workspacePath, session) {
|
|
2710
2995
|
const candidates = [];
|
|
2711
2996
|
for (const filePath of session.plan?.filesLikelyChanged ?? []) {
|
|
2712
2997
|
const normalized = normalizeWorkspaceRelativePath(workspacePath, filePath, "skip");
|
|
2713
2998
|
if (normalized && normalized !== ".") {
|
|
2714
|
-
candidates.push(
|
|
2999
|
+
candidates.push({
|
|
3000
|
+
filePath: normalized,
|
|
3001
|
+
score: 80,
|
|
3002
|
+
reason: "AI plan marked this file as likely impacted."
|
|
3003
|
+
});
|
|
2715
3004
|
}
|
|
2716
3005
|
}
|
|
2717
|
-
candidates.push("package.json", "README.md", "AGENTS.md");
|
|
3006
|
+
candidates.push({ filePath: "package.json", score: 24, reason: "Package metadata helps infer scripts, package boundaries, and runtime." }, { filePath: "README.md", score: 18, reason: "README gives repository purpose and local validation hints." }, { filePath: "AGENTS.md", score: 18, reason: "Agent instructions constrain safe implementation style." }, { filePath: "CODEOWNERS", score: 14, reason: "Ownership metadata helps identify relevant domains and review paths." });
|
|
2718
3007
|
const trackedFiles = await listTrackedWorkspaceFiles(workspacePath);
|
|
2719
3008
|
const tokens = tokenizePatchSearchText([
|
|
2720
3009
|
session.workItem.key,
|
|
2721
3010
|
session.workItem.title,
|
|
2722
3011
|
session.workItem.description,
|
|
3012
|
+
session.plan?.summary,
|
|
3013
|
+
...(session.plan?.steps.map((step) => `${step.title} ${step.detail ?? ""}`) ?? []),
|
|
2723
3014
|
...(session.workItem.labels ?? []),
|
|
2724
|
-
...(session.workItem.components ?? [])
|
|
3015
|
+
...(session.workItem.components ?? []),
|
|
3016
|
+
...(session.workspaceCandidate?.workspace.packageNames ?? []),
|
|
3017
|
+
...(session.workspaceCandidate?.workspace.readmeKeywords ?? [])
|
|
2725
3018
|
].filter((value) => Boolean(value)).join(" "));
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
3019
|
+
const planHints = new Set((session.plan?.filesLikelyChanged ?? [])
|
|
3020
|
+
.map((filePath) => normalizeWorkspaceRelativePath(workspacePath, filePath, "skip"))
|
|
3021
|
+
.filter((filePath) => Boolean(filePath)));
|
|
3022
|
+
const rankedTrackedFiles = trackedFiles
|
|
3023
|
+
.map((filePath) => ({
|
|
3024
|
+
filePath,
|
|
3025
|
+
score: scorePatchContextFile(filePath, tokens, planHints),
|
|
3026
|
+
reason: describePatchContextReason(filePath, tokens, planHints)
|
|
3027
|
+
}))
|
|
3028
|
+
.filter((candidate) => candidate.score > 0)
|
|
3029
|
+
.sort((left, right) => right.score - left.score || left.filePath.localeCompare(right.filePath));
|
|
3030
|
+
for (const candidate of rankedTrackedFiles.slice(0, 40)) {
|
|
3031
|
+
candidates.push(candidate);
|
|
2731
3032
|
}
|
|
2732
3033
|
const selected = [];
|
|
2733
3034
|
const seen = new Set();
|
|
@@ -2736,7 +3037,7 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2736
3037
|
if (selected.length >= maxPatchContextFiles || totalBytes >= maxPatchContextTotalBytes) {
|
|
2737
3038
|
break;
|
|
2738
3039
|
}
|
|
2739
|
-
const relativePath = normalizeWorkspaceRelativePath(workspacePath, candidate, "skip");
|
|
3040
|
+
const relativePath = normalizeWorkspaceRelativePath(workspacePath, candidate.filePath, "skip");
|
|
2740
3041
|
if (!relativePath || seen.has(relativePath) || isSensitiveWorkspacePath(relativePath)) {
|
|
2741
3042
|
continue;
|
|
2742
3043
|
}
|
|
@@ -2744,7 +3045,7 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2744
3045
|
const absolutePath = resolve(workspacePath, relativePath);
|
|
2745
3046
|
try {
|
|
2746
3047
|
const content = await readFile(absolutePath, "utf8");
|
|
2747
|
-
if (content.includes("\u0000")) {
|
|
3048
|
+
if (content.includes("\u0000") || containsSensitiveContent(content)) {
|
|
2748
3049
|
continue;
|
|
2749
3050
|
}
|
|
2750
3051
|
const remainingBytes = maxPatchContextTotalBytes - totalBytes;
|
|
@@ -2754,7 +3055,9 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2754
3055
|
selected.push({
|
|
2755
3056
|
path: relativePath,
|
|
2756
3057
|
content: sliced,
|
|
2757
|
-
truncated: Buffer.byteLength(content, "utf8") > Buffer.byteLength(sliced, "utf8")
|
|
3058
|
+
truncated: Buffer.byteLength(content, "utf8") > Buffer.byteLength(sliced, "utf8"),
|
|
3059
|
+
score: candidate.score,
|
|
3060
|
+
reason: candidate.reason
|
|
2758
3061
|
});
|
|
2759
3062
|
}
|
|
2760
3063
|
catch {
|
|
@@ -2765,31 +3068,121 @@ async function collectPatchContextFiles(workspacePath, session) {
|
|
|
2765
3068
|
}
|
|
2766
3069
|
async function listTrackedWorkspaceFiles(workspacePath) {
|
|
2767
3070
|
const output = await runGit(workspacePath, ["ls-files"]);
|
|
2768
|
-
|
|
3071
|
+
const trackedFiles = output
|
|
2769
3072
|
.split(/\r?\n/u)
|
|
2770
3073
|
.map((line) => line.trim())
|
|
2771
3074
|
.filter(Boolean)
|
|
2772
3075
|
.filter((filePath) => !isSensitiveWorkspacePath(filePath))
|
|
2773
3076
|
.slice(0, 1000);
|
|
3077
|
+
return trackedFiles.length > 0 ? trackedFiles : listWorkspaceFilesFallback(workspacePath);
|
|
3078
|
+
}
|
|
3079
|
+
async function listWorkspaceFilesFallback(workspacePath) {
|
|
3080
|
+
const collected = [];
|
|
3081
|
+
const queue = ["."];
|
|
3082
|
+
const ignoredDirectories = new Set([".git", "node_modules", "dist", "build", "coverage", ".next", ".turbo", ".cache", "vendor"]);
|
|
3083
|
+
while (queue.length > 0 && collected.length < 1000) {
|
|
3084
|
+
const current = queue.shift() ?? ".";
|
|
3085
|
+
const absoluteCurrent = resolve(workspacePath, current);
|
|
3086
|
+
let directory;
|
|
3087
|
+
try {
|
|
3088
|
+
directory = await opendir(absoluteCurrent);
|
|
3089
|
+
}
|
|
3090
|
+
catch {
|
|
3091
|
+
continue;
|
|
3092
|
+
}
|
|
3093
|
+
for await (const entry of directory) {
|
|
3094
|
+
const relativePath = current === "." ? entry.name : `${current}/${entry.name}`;
|
|
3095
|
+
if (entry.isDirectory()) {
|
|
3096
|
+
if (!ignoredDirectories.has(entry.name) && !isSensitiveWorkspacePath(relativePath)) {
|
|
3097
|
+
queue.push(relativePath);
|
|
3098
|
+
}
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
if (entry.isFile() && !isSensitiveWorkspacePath(relativePath)) {
|
|
3102
|
+
collected.push(relativePath);
|
|
3103
|
+
if (collected.length >= 1000) {
|
|
3104
|
+
break;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
return collected;
|
|
2774
3110
|
}
|
|
2775
3111
|
function tokenizePatchSearchText(value) {
|
|
2776
3112
|
return Array.from(new Set(value.toLowerCase().split(/[^a-z0-9]+/u).filter((token) => token.length >= 3))).slice(0, 20);
|
|
2777
3113
|
}
|
|
3114
|
+
function scorePatchContextFile(filePath, tokens, planHints) {
|
|
3115
|
+
const lower = filePath.toLowerCase();
|
|
3116
|
+
let score = 0;
|
|
3117
|
+
if (planHints.has(filePath)) {
|
|
3118
|
+
score += 40;
|
|
3119
|
+
}
|
|
3120
|
+
for (const token of tokens) {
|
|
3121
|
+
if (lower.includes(token)) {
|
|
3122
|
+
score += 5;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
if (/\.(ts|tsx|js|jsx|mjs|cjs|py|java|kt|go|rs|rb|php|cs|swift)$/u.test(lower)) {
|
|
3126
|
+
score += 4;
|
|
3127
|
+
}
|
|
3128
|
+
if (/(src|app|lib|packages|services|connectors|components|routes|api)\//u.test(lower)) {
|
|
3129
|
+
score += 4;
|
|
3130
|
+
}
|
|
3131
|
+
if (/(test|spec|__tests__|tests)\b/u.test(lower)) {
|
|
3132
|
+
score += 5;
|
|
3133
|
+
}
|
|
3134
|
+
if (/(readme|package\.json|codeowners|agents\.md|tsconfig|vite|jest|vitest|pytest|gradle|pom\.xml|go\.mod|cargo\.toml)/u.test(lower)) {
|
|
3135
|
+
score += 2;
|
|
3136
|
+
}
|
|
3137
|
+
if (/(dist|build|coverage|node_modules|vendor|generated|\.lock$|lockfile|\.min\.)/u.test(lower)) {
|
|
3138
|
+
score -= 16;
|
|
3139
|
+
}
|
|
3140
|
+
if (/(snapshot|snapshots|fixtures|fixture|mock|mocks)\//u.test(lower)) {
|
|
3141
|
+
score -= 2;
|
|
3142
|
+
}
|
|
3143
|
+
return score;
|
|
3144
|
+
}
|
|
3145
|
+
function describePatchContextReason(filePath, tokens, planHints) {
|
|
3146
|
+
const lower = filePath.toLowerCase();
|
|
3147
|
+
const reasons = [
|
|
3148
|
+
planHints.has(filePath) ? "named by the approved plan" : undefined,
|
|
3149
|
+
tokens.filter((token) => lower.includes(token)).slice(0, 4).length
|
|
3150
|
+
? `matches task token(s): ${tokens.filter((token) => lower.includes(token)).slice(0, 4).join(", ")}`
|
|
3151
|
+
: undefined,
|
|
3152
|
+
/(test|spec|__tests__|tests)\b/u.test(lower) ? "is a related validation file" : undefined,
|
|
3153
|
+
/(package\.json|tsconfig|vite|jest|vitest|pytest|gradle|pom\.xml|go\.mod|cargo\.toml)/u.test(lower) ? "contains project or test configuration" : undefined,
|
|
3154
|
+
/(readme|codeowners|agents\.md)/u.test(lower) ? "contains repository guidance" : undefined,
|
|
3155
|
+
/\.(ts|tsx|js|jsx|mjs|cjs|py|java|kt|go|rs|rb|php|cs|swift)$/u.test(lower) ? "is source code in a supported language" : undefined
|
|
3156
|
+
].filter((reason) => Boolean(reason));
|
|
3157
|
+
return reasons.length ? reasons.join("; ") : "ranked from repository metadata and work item text";
|
|
3158
|
+
}
|
|
2778
3159
|
function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
2779
3160
|
const plan = session.plan;
|
|
2780
3161
|
const context = contextFiles.map((file) => [
|
|
2781
3162
|
`FILE: ${file.path}${file.truncated ? " (truncated)" : ""}`,
|
|
3163
|
+
`RANK: ${file.score}`,
|
|
3164
|
+
`WHY_INCLUDED: ${file.reason}`,
|
|
2782
3165
|
"```",
|
|
2783
3166
|
file.content,
|
|
2784
3167
|
"```"
|
|
2785
3168
|
].join("\n")).join("\n\n");
|
|
2786
3169
|
const failedTestContext = getFailedTestContextAfterLatestPatch(session);
|
|
3170
|
+
const missingRequirementSignals = Array.from(new Set([
|
|
3171
|
+
...detectMissingRequirementSignals(session.workItem),
|
|
3172
|
+
...(plan?.missingInfo ?? [])
|
|
3173
|
+
])).slice(0, 8);
|
|
3174
|
+
const workspace = session.workspaceCandidate?.workspace;
|
|
2787
3175
|
return [
|
|
2788
3176
|
"You are OpenPome's implementation engine.",
|
|
3177
|
+
failedTestContext.length
|
|
3178
|
+
? "This is a retry after approved validation failed. Repair only the failure using the evidence below."
|
|
3179
|
+
: "This is the first implementation patch for the approved plan.",
|
|
2789
3180
|
"Return only compact JSON. Do not include markdown fences outside JSON.",
|
|
2790
3181
|
"Only propose a minimal safe file patch for the approved work item.",
|
|
2791
3182
|
"Do not include secrets, credentials, generated dependency folders, lockfile rewrites, or unrelated refactors.",
|
|
2792
3183
|
"Use full replacement file content for each proposed file.",
|
|
3184
|
+
"If requirements are unclear, prefer a small diagnostic or guardrail change over a speculative broad rewrite.",
|
|
3185
|
+
"Keep existing style, imports, formatting, and public contracts unless the work item clearly requires a change.",
|
|
2793
3186
|
"Allowed JSON shape:",
|
|
2794
3187
|
"{\"summary\":\"...\",\"files\":[{\"path\":\"relative/path\",\"action\":\"create|update\",\"content\":\"full file content\"}],\"risks\":[\"...\"]}",
|
|
2795
3188
|
"",
|
|
@@ -2802,7 +3195,11 @@ function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
|
2802
3195
|
session.workItem.priority ? `- Priority: ${session.workItem.priority}` : undefined,
|
|
2803
3196
|
session.workItem.labels?.length ? `- Labels: ${session.workItem.labels.join(", ")}` : undefined,
|
|
2804
3197
|
session.workItem.components?.length ? `- Components: ${session.workItem.components.join(", ")}` : undefined,
|
|
3198
|
+
session.workItem.links?.length ? `- Links: ${session.workItem.links.map((link) => `${link.kind}:${link.title ?? link.url}`).join("; ")}` : undefined,
|
|
2805
3199
|
"",
|
|
3200
|
+
missingRequirementSignals.length ? "Known missing or unclear requirements:" : undefined,
|
|
3201
|
+
...missingRequirementSignals.map((signal) => `- ${signal}`),
|
|
3202
|
+
missingRequirementSignals.length ? "" : undefined,
|
|
2806
3203
|
"Approved plan:",
|
|
2807
3204
|
plan?.summary ? `- Summary: ${plan.summary}` : "- Summary: unavailable",
|
|
2808
3205
|
...(plan?.steps.map((step) => `- ${step.title}${step.detail ? `: ${step.detail}` : ""}`) ?? []),
|
|
@@ -2813,7 +3210,13 @@ function buildStructuredPatchPrompt(session, workspacePath, contextFiles) {
|
|
|
2813
3210
|
failedTestContext.length ? "" : undefined,
|
|
2814
3211
|
"Workspace:",
|
|
2815
3212
|
`- Path: ${workspacePath}`,
|
|
2816
|
-
|
|
3213
|
+
workspace?.name ? `- Name: ${workspace.name}` : undefined,
|
|
3214
|
+
workspace?.currentBranch ? `- Current branch: ${workspace.currentBranch}` : undefined,
|
|
3215
|
+
workspace?.packageNames?.length ? `- Packages: ${workspace.packageNames.slice(0, 8).join(", ")}` : undefined,
|
|
3216
|
+
workspace?.readmeKeywords?.length ? `- README signals: ${workspace.readmeKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
3217
|
+
workspace?.codeownersKeywords?.length ? `- Ownership signals: ${workspace.codeownersKeywords.slice(0, 12).join(", ")}` : undefined,
|
|
3218
|
+
workspace?.recentBranches?.length ? `- Recent branches: ${workspace.recentBranches.slice(0, 8).join(", ")}` : undefined,
|
|
3219
|
+
workspace?.recentCommitRefs?.length ? `- Recent work refs: ${workspace.recentCommitRefs.slice(0, 12).join(", ")}` : undefined,
|
|
2817
3220
|
"",
|
|
2818
3221
|
"Readable context files:",
|
|
2819
3222
|
context || "No source files were safely included. You may propose small new files only if the task clearly asks for them."
|
|
@@ -2860,7 +3263,7 @@ function parseAIPatchProposal(value, session, provider, workspacePath, createdAt
|
|
|
2860
3263
|
const relativePath = normalizeWorkspaceRelativePath(workspacePath, maybe.path, "throw");
|
|
2861
3264
|
const action = maybe.action === "create" ? "create" : "update";
|
|
2862
3265
|
const content = maybe.content;
|
|
2863
|
-
if (!relativePath || isSensitiveWorkspacePath(relativePath) || content.includes("\u0000")) {
|
|
3266
|
+
if (!relativePath || isSensitiveWorkspacePath(relativePath) || content.includes("\u0000") || containsSensitiveContent(content)) {
|
|
2864
3267
|
return undefined;
|
|
2865
3268
|
}
|
|
2866
3269
|
if (Buffer.byteLength(content, "utf8") > maxPatchProposalBytesPerFile) {
|
|
@@ -2908,6 +3311,9 @@ function isSensitiveWorkspacePath(filePath) {
|
|
|
2908
3311
|
}
|
|
2909
3312
|
return sensitivePathFragments.some((fragment) => lower === fragment || lower.includes(`/${fragment}`) || lower.endsWith(fragment));
|
|
2910
3313
|
}
|
|
3314
|
+
function containsSensitiveContent(content) {
|
|
3315
|
+
return sensitiveContentPatterns.some((pattern) => pattern.test(content));
|
|
3316
|
+
}
|
|
2911
3317
|
async function applyPatchFiles(workspacePath, files) {
|
|
2912
3318
|
for (const file of files) {
|
|
2913
3319
|
const relativePath = normalizeWorkspaceRelativePath(workspacePath, file.path, "throw");
|
|
@@ -2919,7 +3325,7 @@ async function applyPatchFiles(workspacePath, files) {
|
|
|
2919
3325
|
await writeFile(absolutePath, file.content, "utf8");
|
|
2920
3326
|
}
|
|
2921
3327
|
}
|
|
2922
|
-
async function discoverTestCommandCandidates(workspacePath) {
|
|
3328
|
+
async function discoverTestCommandCandidates(workspacePath, session) {
|
|
2923
3329
|
const scripts = await readPackageScripts(join(workspacePath, "package.json"));
|
|
2924
3330
|
const packageManager = detectPackageManager(workspacePath);
|
|
2925
3331
|
const candidates = [];
|
|
@@ -2935,6 +3341,17 @@ async function discoverTestCommandCandidates(workspacePath) {
|
|
|
2935
3341
|
cwd: workspacePath
|
|
2936
3342
|
});
|
|
2937
3343
|
}
|
|
3344
|
+
const relatedTestFiles = session ? await findRelatedTestFiles(workspacePath, session) : [];
|
|
3345
|
+
const testScript = scripts["test"] ? buildPackageScriptCommand(packageManager, "test") : undefined;
|
|
3346
|
+
for (const testFile of relatedTestFiles.slice(0, 5)) {
|
|
3347
|
+
candidates.push({
|
|
3348
|
+
id: `related_${createHash("sha256").update(testFile).digest("hex").slice(0, 8)}`,
|
|
3349
|
+
command: testScript ? `${testScript} -- ${quoteShellArg(testFile)}` : buildLanguageSpecificTestCommand(packageManager, testFile),
|
|
3350
|
+
source: "related_file",
|
|
3351
|
+
reason: `Related test file matched likely impacted work: ${testFile}.`,
|
|
3352
|
+
cwd: workspacePath
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
2938
3355
|
if (candidates.length > 0) {
|
|
2939
3356
|
return candidates;
|
|
2940
3357
|
}
|
|
@@ -2951,6 +3368,74 @@ async function discoverTestCommandCandidates(workspacePath) {
|
|
|
2951
3368
|
}
|
|
2952
3369
|
return getFallbackTestCommandCandidates(workspacePath);
|
|
2953
3370
|
}
|
|
3371
|
+
async function findRelatedTestFiles(workspacePath, session) {
|
|
3372
|
+
let trackedFiles = [];
|
|
3373
|
+
try {
|
|
3374
|
+
trackedFiles = await listTrackedWorkspaceFiles(workspacePath);
|
|
3375
|
+
}
|
|
3376
|
+
catch {
|
|
3377
|
+
return [];
|
|
3378
|
+
}
|
|
3379
|
+
const impactHints = new Set([
|
|
3380
|
+
...(session.plan?.filesLikelyChanged ?? [])
|
|
3381
|
+
.map((filePath) => normalizeWorkspaceRelativePath(workspacePath, filePath, "skip"))
|
|
3382
|
+
.filter((filePath) => Boolean(filePath))
|
|
3383
|
+
.flatMap((filePath) => [filePath, basename(filePath).replace(/\.[^.]+$/u, "")]),
|
|
3384
|
+
...tokenizePatchSearchText([
|
|
3385
|
+
session.workItem.key,
|
|
3386
|
+
session.workItem.title,
|
|
3387
|
+
session.workItem.description,
|
|
3388
|
+
...(session.workItem.labels ?? []),
|
|
3389
|
+
...(session.workItem.components ?? []),
|
|
3390
|
+
session.plan?.summary
|
|
3391
|
+
].filter((value) => Boolean(value)).join(" "))
|
|
3392
|
+
]);
|
|
3393
|
+
return trackedFiles
|
|
3394
|
+
.filter((filePath) => isTestLikeFile(filePath))
|
|
3395
|
+
.map((filePath) => ({
|
|
3396
|
+
filePath,
|
|
3397
|
+
score: scoreRelatedTestFile(filePath, impactHints)
|
|
3398
|
+
}))
|
|
3399
|
+
.filter((candidate) => candidate.score > 0)
|
|
3400
|
+
.sort((left, right) => right.score - left.score || left.filePath.localeCompare(right.filePath))
|
|
3401
|
+
.map((candidate) => candidate.filePath);
|
|
3402
|
+
}
|
|
3403
|
+
function isTestLikeFile(filePath) {
|
|
3404
|
+
return /(^|\/)(__tests__|tests?|specs?)\//u.test(filePath.toLowerCase())
|
|
3405
|
+
|| /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py|java|kt|go|rs|rb|php|cs)$/u.test(filePath.toLowerCase());
|
|
3406
|
+
}
|
|
3407
|
+
function scoreRelatedTestFile(filePath, impactHints) {
|
|
3408
|
+
const lower = filePath.toLowerCase();
|
|
3409
|
+
let score = 0;
|
|
3410
|
+
for (const hint of impactHints) {
|
|
3411
|
+
const normalized = hint.toLowerCase();
|
|
3412
|
+
if (normalized.length >= 3 && lower.includes(normalized)) {
|
|
3413
|
+
score += normalized.includes("/") ? 10 : 4;
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
if (score === 0) {
|
|
3417
|
+
return 0;
|
|
3418
|
+
}
|
|
3419
|
+
if (/\.(test|spec)\./u.test(lower)) {
|
|
3420
|
+
score += 3;
|
|
3421
|
+
}
|
|
3422
|
+
if (/(__tests__|tests?)\//u.test(lower)) {
|
|
3423
|
+
score += 2;
|
|
3424
|
+
}
|
|
3425
|
+
if (/(snapshot|fixtures|mocks)\//u.test(lower)) {
|
|
3426
|
+
score -= 2;
|
|
3427
|
+
}
|
|
3428
|
+
return score;
|
|
3429
|
+
}
|
|
3430
|
+
function buildLanguageSpecificTestCommand(packageManager, testFile) {
|
|
3431
|
+
if (/\.(py)$/u.test(testFile)) {
|
|
3432
|
+
return `python -m pytest ${quoteShellArg(testFile)}`;
|
|
3433
|
+
}
|
|
3434
|
+
if (/\.(go)$/u.test(testFile)) {
|
|
3435
|
+
return "go test ./...";
|
|
3436
|
+
}
|
|
3437
|
+
return `${buildPackageScriptCommand(packageManager, "test")} -- ${quoteShellArg(testFile)}`;
|
|
3438
|
+
}
|
|
2954
3439
|
function detectPackageManager(workspacePath) {
|
|
2955
3440
|
if (existsSync(join(workspacePath, "pnpm-lock.yaml"))) {
|
|
2956
3441
|
return "pnpm";
|
|
@@ -2975,6 +3460,9 @@ function buildPackageScriptCommand(packageManager, scriptName) {
|
|
|
2975
3460
|
}
|
|
2976
3461
|
return `pnpm ${scriptName}`;
|
|
2977
3462
|
}
|
|
3463
|
+
function quoteShellArg(value) {
|
|
3464
|
+
return `'${value.replace(/'/gu, "'\\''")}'`;
|
|
3465
|
+
}
|
|
2978
3466
|
function getFallbackTestCommandCandidates(cwd) {
|
|
2979
3467
|
return [
|
|
2980
3468
|
{
|
|
@@ -3075,6 +3563,14 @@ async function ensurePullRequestBranch(workspacePath, branch) {
|
|
|
3075
3563
|
}
|
|
3076
3564
|
return branch;
|
|
3077
3565
|
}
|
|
3566
|
+
async function pushPullRequestBranch(workspacePath, branch) {
|
|
3567
|
+
try {
|
|
3568
|
+
await runGitStrict(workspacePath, ["push", "-u", "origin", branch]);
|
|
3569
|
+
}
|
|
3570
|
+
catch (error) {
|
|
3571
|
+
throw new Error(getGitHubCliGuidance("push pull request branch", error));
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3078
3574
|
async function createGitHubPullRequestWithCli(workspacePath, draft, branch, draftPr) {
|
|
3079
3575
|
const ghArgs = [
|
|
3080
3576
|
"pr",
|
|
@@ -3091,14 +3587,19 @@ async function createGitHubPullRequestWithCli(workspacePath, draft, branch, draf
|
|
|
3091
3587
|
if (draftPr) {
|
|
3092
3588
|
ghArgs.push("--draft");
|
|
3093
3589
|
}
|
|
3094
|
-
|
|
3590
|
+
try {
|
|
3591
|
+
return (await execFileStrict("gh", ghArgs, workspacePath)).trim();
|
|
3592
|
+
}
|
|
3593
|
+
catch (error) {
|
|
3594
|
+
throw new Error(getGitHubCliGuidance("create pull request", error));
|
|
3595
|
+
}
|
|
3095
3596
|
}
|
|
3096
3597
|
async function createGitHubPullRequestWithApi(accessToken, draft, branch, draftPr) {
|
|
3097
3598
|
const repository = parseGitHubRepositoryCoordinates(draft.remoteUrl);
|
|
3098
3599
|
if (!repository) {
|
|
3099
3600
|
throw new Error("Unable to determine GitHub owner/repo from the workspace remote URL.");
|
|
3100
3601
|
}
|
|
3101
|
-
const response = await
|
|
3602
|
+
const response = await fetchGitHub(`${getGitHubApiBaseUrl()}/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/pulls`, {
|
|
3102
3603
|
method: "POST",
|
|
3103
3604
|
headers: {
|
|
3104
3605
|
Accept: "application/vnd.github+json",
|
|
@@ -3113,14 +3614,22 @@ async function createGitHubPullRequestWithApi(accessToken, draft, branch, draftP
|
|
|
3113
3614
|
base: draft.baseBranch,
|
|
3114
3615
|
draft: draftPr
|
|
3115
3616
|
})
|
|
3116
|
-
});
|
|
3617
|
+
}, "create pull request");
|
|
3117
3618
|
if (!response.ok) {
|
|
3118
|
-
|
|
3119
|
-
throw new Error(`GitHub PR creation failed: ${response.status} ${response.statusText}${body ? `: ${body}` : ""}`);
|
|
3619
|
+
throw new Error(await getGitHubStatusGuidance(response, "create pull request"));
|
|
3120
3620
|
}
|
|
3121
3621
|
const payload = (await response.json());
|
|
3122
3622
|
return payload.html_url ?? `https://github.com/${repository.owner}/${repository.repo}/pull/${payload.number ?? ""}`;
|
|
3123
3623
|
}
|
|
3624
|
+
function getGitHubCliGuidance(action, error) {
|
|
3625
|
+
const detail = summarizeExecError(error) ?? String(error);
|
|
3626
|
+
return [
|
|
3627
|
+
`GitHub ${action} failed.`,
|
|
3628
|
+
"Check repository write permission, organization SSO authorization, branch protection, remote URL, and whether your token/SSH key can push to origin.",
|
|
3629
|
+
"Run `pome auth github status` and `git remote -v` to verify the account and repository.",
|
|
3630
|
+
`Detail: ${detail}`
|
|
3631
|
+
].join(" ");
|
|
3632
|
+
}
|
|
3124
3633
|
function parseGitHubRepositoryCoordinates(remoteUrl) {
|
|
3125
3634
|
if (!remoteUrl) {
|
|
3126
3635
|
return undefined;
|
|
@@ -3169,6 +3678,33 @@ async function safeResponseText(response) {
|
|
|
3169
3678
|
return "";
|
|
3170
3679
|
}
|
|
3171
3680
|
}
|
|
3681
|
+
function summarizeProviderBody(value) {
|
|
3682
|
+
const trimmed = value.trim();
|
|
3683
|
+
if (!trimmed) {
|
|
3684
|
+
return "";
|
|
3685
|
+
}
|
|
3686
|
+
try {
|
|
3687
|
+
const parsed = JSON.parse(trimmed);
|
|
3688
|
+
const errorObject = typeof parsed.error === "object" && parsed.error
|
|
3689
|
+
? parsed.error
|
|
3690
|
+
: undefined;
|
|
3691
|
+
const messages = [
|
|
3692
|
+
typeof parsed.message === "string" ? parsed.message : undefined,
|
|
3693
|
+
typeof parsed.error === "string" ? parsed.error : undefined,
|
|
3694
|
+
typeof errorObject?.message === "string" ? errorObject.message : undefined,
|
|
3695
|
+
typeof errorObject?.type === "string" ? `type=${errorObject.type}` : undefined,
|
|
3696
|
+
typeof errorObject?.code === "string" ? `code=${errorObject.code}` : undefined,
|
|
3697
|
+
typeof parsed.documentation_url === "string" ? parsed.documentation_url : undefined
|
|
3698
|
+
].filter((item) => Boolean(item));
|
|
3699
|
+
if (messages.length) {
|
|
3700
|
+
return messages.join("; ").slice(0, 500);
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
catch {
|
|
3704
|
+
// Fall through to plain-text summary.
|
|
3705
|
+
}
|
|
3706
|
+
return trimmed.replace(/\s+/gu, " ").slice(0, 500);
|
|
3707
|
+
}
|
|
3172
3708
|
async function hasWorkspaceChanges(workspacePath) {
|
|
3173
3709
|
const output = await runGit(workspacePath, ["status", "--porcelain"]);
|
|
3174
3710
|
return output.trim().length > 0;
|