@openpome/local-gateway 0.34.0-alpha.0 → 0.37.0-alpha.0

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