From bc5c392331cd43d530314ecceff910901d785806 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 3 Aug 2026 10:21:33 -0500 Subject: [PATCH] fix(server): stop inferring PR credential preflight from issue text (#10755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip coordinates agent runs and checks required runtime credentials before dispatch. > - The push-capability preflight protects runs that use the GitHub PR workflow skill. > - PR #10659 also made issue title and description text trigger this preflight. > - That text heuristic blocks tasks that mention a pull request but do not need a bound GitHub token at dispatch time. > - This pull request removes the text heuristic and keeps the explicit skill trigger. > - The benefit is that ordinary task wording no longer causes a false configuration failure. ## Linked Issues or Issue Description Refs: #10659 **What happened?** An issue title or description that said to open a pull request or push a branch could trigger the push-capability credential preflight. The run then failed before dispatch when no project-level or agent-level GitHub token was bound, even when the task could proceed without that preflight. **Expected behavior** The preflight must run only when the issue explicitly selects the GitHub PR workflow skill. Issue prose alone must not enable the guard. **Steps to reproduce** 1. Assign a local Codex or Claude agent an issue that says to open a pull request. 2. Do not attach the GitHub PR workflow skill to the issue. 3. Start the run without a project-level or agent-level GitHub token binding. 4. Observe the false `push_write_credential_missing` failure before this fix. **Paperclip version or commit** `master` after PR #10659. **Deployment mode** Local dev with a git-sensitive local adapter. ## What Changed - Removed `issueTextImpliesPrDeliverable` and its text-pattern matcher. - Restored `requiresPushCapabilityPreflight` to use only explicit GitHub PR workflow skill keys. - Removed the obsolete issue-text tests while keeping coverage for the explicit skill, adapter, and issue gates. ## Verification - `PAPERCLIP_LOG_DIR= pnpm vitest run server/src/__tests__/heartbeat-workspace-session.test.ts` — 134 tests passed. - `pnpm --filter @paperclipai/server typecheck` — passed. - `git diff --check origin/master...HEAD` — passed. ## Risks - Low risk. A task that states a PR deliverable but does not select the GitHub PR workflow skill will no longer receive the early credential preflight. This is the intended temporary behavior. - Tasks that explicitly select the skill still receive the existing credential and checkout checks. > This change does not add a core feature and does not overlap with ROADMAP.md work. ## Model Used OpenAI Codex with model ID `gpt-5`. The runtime did not expose the context-window size. The model used agentic reasoning, tool use, and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip --- .../heartbeat-workspace-session.test.ts | 52 ------------------- server/src/services/heartbeat.ts | 33 +----------- 2 files changed, 1 insertion(+), 84 deletions(-) diff --git a/server/src/__tests__/heartbeat-workspace-session.test.ts b/server/src/__tests__/heartbeat-workspace-session.test.ts index 5f94040ce6..26734c687d 100644 --- a/server/src/__tests__/heartbeat-workspace-session.test.ts +++ b/server/src/__tests__/heartbeat-workspace-session.test.ts @@ -30,7 +30,6 @@ import { resolveExecutionWorkspaceReuseProvisioningPolicy, resolveNextSessionState, resolveTaskSessionConfigFreshness, - issueTextImpliesPrDeliverable, isWorkspaceSyncConflictFailure, requiresPushCapabilityPreflight, resolveWorkspaceAfterLowTrustPreflight, @@ -972,58 +971,7 @@ describe("assertPushCapabilityCheckoutValid", () => { }); }); -describe("issueTextImpliesPrDeliverable", () => { - it("matches verb-anchored PR deliverables", () => { - expect(issueTextImpliesPrDeliverable("Review and open PR for the CI shard split")).toBe(true); - expect(issueTextImpliesPrDeliverable("Push the branch and open a pull request")).toBe(true); - expect(issueTextImpliesPrDeliverable("Each run: make the change and open a draft PR")).toBe(true); - expect(issueTextImpliesPrDeliverable("push feature work to origin when done")).toBe(true); - }); - - it("ignores passing mentions and unrelated text", () => { - expect(issueTextImpliesPrDeliverable("The PR merged yesterday; investigate the regression")).toBe(false); - expect(issueTextImpliesPrDeliverable("PR feedback addressed")).toBe(false); - expect(issueTextImpliesPrDeliverable("Update the pricing page copy")).toBe(false); - expect(issueTextImpliesPrDeliverable("a proper approach to pushing back on scope")).toBe(false); - expect(issueTextImpliesPrDeliverable(null)).toBe(false); - expect(issueTextImpliesPrDeliverable("")).toBe(false); - }); - - it("ignores non-git uses of push", () => { - expect(issueTextImpliesPrDeliverable("push back on the upstream dependency change")).toBe(false); - expect(issueTextImpliesPrDeliverable("push back the branch cut date")).toBe(false); - expect(issueTextImpliesPrDeliverable("push notifications for mobile")).toBe(false); - // Git shapes still match. - expect(issueTextImpliesPrDeliverable("pushing the release branch")).toBe(true); - expect(issueTextImpliesPrDeliverable("push feature work to origin when done")).toBe(true); - }); -}); - describe("requiresPushCapabilityPreflight", () => { - it("enables the guard when the issue text states the PR deliverable", () => { - expect(requiresPushCapabilityPreflight({ - adapterType: "codex_local", - issueId: "issue-1", - explicitRunScopedSkillKeys: [], - issueText: "Push ci/shard-split and open PR", - })).toBe(true); - - expect(requiresPushCapabilityPreflight({ - adapterType: "codex_local", - issueId: "issue-1", - explicitRunScopedSkillKeys: [], - issueText: "Investigate why the PR checks were slow", - })).toBe(false); - - // Without an issue there is nothing to preflight. - expect(requiresPushCapabilityPreflight({ - adapterType: "codex_local", - issueId: null, - explicitRunScopedSkillKeys: [], - issueText: "open a PR", - })).toBe(false); - }); - it("only enables the guard when the issue explicitly mentions the GitHub PR workflow skill", () => { expect(requiresPushCapabilityPreflight({ adapterType: "codex_local", diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index f027ab3d9c..e931f6dab9 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -736,44 +736,14 @@ function hasGithubPrWorkflowSkill(desiredSkills: string[]) { }); } -/** - * Conservative, verb-anchored patterns for an issue whose deliverable is a - * pushed branch or opened pull request. Verb anchoring keeps passing mentions - * ("the PR merged yesterday") from triggering the credential preflight. - */ -const PR_DELIVERABLE_TEXT_PATTERNS = [ - /\bopen(?:s|ed|ing)?\s+(?:a\s+|the\s+|an?\s+draft\s+)?(?:pull\s+request|pr)\b/i, - /\b(?:create|creates|created|creating|raise|raises|raised|raising|submit|submits|submitted|submitting)\s+(?:a\s+|the\s+|an?\s+draft\s+)?(?:pull\s+request|pr)\b/i, - // "push back" (an objection or a date) is never a git push, and bare - // proximity to words like "upstream" over-matches ("push back on the - // upstream dependency change"); require the git object shape instead. - /\bpush(?:es|ed|ing)?\b(?!\s+back\b)[^.\n]{0,40}\bbranch(?:es)?\b/i, - /\bpush(?:es|ed|ing)?\s+(?:[^.\n]{0,30}\s)?to\s+(?:origin|remote|upstream|github)\b/i, -]; - -export function issueTextImpliesPrDeliverable(text: string | null | undefined): boolean { - if (!text) return false; - return PR_DELIVERABLE_TEXT_PATTERNS.some((pattern) => pattern.test(text)); -} - export function requiresPushCapabilityPreflight(input: { adapterType: string; issueId: string | null | undefined; explicitRunScopedSkillKeys: string[]; - /** - * Issue title + description. Routine-created issues and agent-to-agent - * handoffs rarely mention the GitHub PR workflow skill explicitly, yet - * state the PR deliverable in plain text — without this, the credential - * gap only surfaces after the implementation and review work is done. - */ - issueText?: string | null; }) { return Boolean(input.issueId) && GIT_SENSITIVE_LOCAL_ADAPTER_TYPES.has(input.adapterType) - && ( - hasGithubPrWorkflowSkill(input.explicitRunScopedSkillKeys) - || issueTextImpliesPrDeliverable(input.issueText) - ); + && hasGithubPrWorkflowSkill(input.explicitRunScopedSkillKeys); } const LOW_TRUST_SENSITIVE_ENV_KEY_RE = @@ -13840,7 +13810,6 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) adapterType: agent.adapterType, issueId, explicitRunScopedSkillKeys: runScopedMentionedSkillKeys, - issueText: issueRef ? `${issueRef.title ?? ""}\n${issueRef.description ?? ""}` : null, }); const { resolvedConfig, secretKeys, secretManifest } = await resolveExecutionRunAdapterConfig({ companyId: agent.companyId,