From 06c08e8f3231b1f42e5646da59da592b00f04021 Mon Sep 17 00:00:00 2001 From: Jonathan Liebig Date: Sat, 10 Oct 2026 00:01:17 +0200 Subject: [PATCH 1/4] docs: add native GitHub context workflow --- CHANGELOG.md | 4 ++++ README.md | 36 ++++++++++++++++++++++++++++++++---- docs/manual-tests.md | 33 +++++++++++++++++++++++++++++++++ docs/windows-work.md | 6 ++++++ skills/ask-pro/SKILL.md | 40 ++++++++++++++++++++++++++++++++++++++-- 5 files changed, 113 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c713271108..e94b86ad68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ### Added +- Document optional native ChatGPT GitHub connection and teach the ask-pro skill + to request read-only repository evidence, preserve local-file context, and + report unavailable access. Include an opt-in retrieval smoke; no live GitHub + retrieval has been verified yet. - Add skills CLI installation for Codex and other supported agents using the standalone `ask-pro` npm CLI. - Publish `ask-pro` to npm after each passing main-branch CI run. diff --git a/README.md b/README.md index 97aa1ca211..38d3f2acf0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # ask-pro `ask-pro` gives coding agents a focused ChatGPT Pro second opinion through a -human-logged-in browser. The calling agent supplies the context and owns the -result; ask-pro never applies generated code. +human-logged-in browser. The calling agent supplies the question and evidence, +and owns the result; ask-pro never applies generated code. ## Install @@ -72,8 +72,9 @@ ask-pro --no-temporary --prompt-file question.md --files src --files tests ``` ask-pro selects `Latest` when available, otherwise the highest numbered available -GPT model, with `Pro` intelligence. It has no repo or conversation -context unless you provide it, so keep the prompt and file bundle focused. +GPT model, with `Pro` intelligence. Supply the caller's context explicitly. +Use a focused file bundle for local code; connected GitHub can provide remote +repository context when available (see below). Request generated files only when needed; treat them as data and never execute them automatically: @@ -87,6 +88,33 @@ Use `ask-pro --help` for the full CLI. For multiline prompts, use `--prompt-file`. Prefer `--no-temporary` when recovery matters; `--temporary` requires Temporary Chat. The [skill](skills/ask-pro/SKILL.md) has agent guidance. +## Optional GitHub context + +You can connect GitHub to the ChatGPT account used by ask-pro. OpenAI documents +the native [GitHub app](https://help.openai.com/en/articles/11145903-connecting-github-to-chatgpt) +as read-only. Access depends on the account, workspace, and ChatGPT experience; +a connection alone does not prove it works with ask-pro's selected Pro model. + +Before starting an ask-pro run, open ChatGPT in your usual browser with the same +account and workspace. In **Settings > Plugins** (or **Apps**, if shown), open +GitHub and complete its connection flow yourself. Review permissions and select +only the repositories you want available; an organization may require approval. +Do this separately from ask-pro's login pause, which automatically continues +once ChatGPT sign-in succeeds. + +Then explicitly request repository evidence in the prompt: + +```powershell +ask-pro --no-temporary "Use the connected GitHub app to read owner/repo. Explain its request authentication flow, cite the files and revision you actually retrieved, and report any access gaps. Do not change anything." +``` + +Use `--files` for local changes or a precise snapshot that GitHub cannot retrieve. +ask-pro forwards the request; it does not connect the app, select an app in the +composer, approve app actions, or verify that retrieval succeeded. Check the +answer's evidence. The opt-in [GitHub smoke](docs/manual-tests.md#connected-github-context) +describes how to verify the path on your account. GitHub authorization stays with +ChatGPT; ask-pro needs no GitHub token. + ## Development ```powershell diff --git a/docs/manual-tests.md b/docs/manual-tests.md index 3273bcf450..aec25d4705 100644 --- a/docs/manual-tests.md +++ b/docs/manual-tests.md @@ -54,6 +54,39 @@ Expected: exist. - `status.json` reports a dry-run/prepared state. +## Connected GitHub Context + +This is an opt-in live test. Complete the README's GitHub setup first, separately +from an ask-pro run. Use the same ChatGPT account and workspace and authorize +only the intended repository. Do not grant broader access just to make the test +pass. No GitHub retrieval smoke has been recorded for this workflow yet. + +1. Pick an authorized repository and a file with a specific fact you can verify + independently, such as the behavior of a named function. Record the expected + fact and commit locally, without including the answer in the prompt. +2. Write `github-question.md`, naming `owner/repo`, the path, requested commit, + and question. Ask Pro to retrieve the file through the connected GitHub app, + cite the source and revision actually read, make no changes, and explicitly + report unavailable access or a revision mismatch. +3. Run without `--files`, so the answer cannot come from an uploaded source file: + + ```powershell + ask-pro --no-temporary --prompt-file github-question.md + ask-pro --harvest + ``` + +4. Compare the answer against the independently recorded file. After the run has + finished, inspect the saved conversation's tool activity for GitHub retrieval + evidence. A plausible answer, a public web search, or `COMPLETED` alone is not + proof of connected-app use. Record the session, model, repository/ref, and + retrieval evidence without credentials or private source contents. + +If GitHub is unavailable, requires manual app selection/approval, or cannot read +the requested revision, record the limitation; the CLI does not automate those +steps. Keep Pro selected and use `--files` for the consult instead. A separate +manual ChatGPT test does not prove ask-pro can invoke the app. Do not interact +with the guarded run window while Pro is answering. + ## First Login / Auth Gate Run a tiny prompt: diff --git a/docs/windows-work.md b/docs/windows-work.md index 5a0489acd1..19ff4851d7 100644 --- a/docs/windows-work.md +++ b/docs/windows-work.md @@ -28,6 +28,12 @@ Read this when working on `ask_pro` from Windows and add new findings here. follows a replacement DevTools port recorded by the managed profile. Challenge recovery requires a visible active challenge control or surface; queued composer text and class or ID substrings do not trigger it. +- Optional GitHub connection happens in ChatGPT settings before starting a run, + using the same account and workspace as the managed browser. It can be set up + in the user's usual browser; do not copy browser cookies or GitHub tokens. + The managed login recovery automatically resumes on sign-in, so it is not a + pause for app setup. GitHub retrieval through the Pro path still needs the + opt-in check in `docs/manual-tests.md` on the target account. - Chrome DevTools state is recorded in each session's `browser.json`; use the saved port for DOM inspection when a live browser needs debugging. - Concurrent fresh and resumed runs on one managed profile use PID-backed diff --git a/skills/ask-pro/SKILL.md b/skills/ask-pro/SKILL.md index 84fa6a2923..51831a01f8 100644 --- a/skills/ask-pro/SKILL.md +++ b/skills/ask-pro/SKILL.md @@ -34,7 +34,8 @@ When invoked: 1. Inspect the repo and the relevant files. 2. Identify the exact decision Pro should answer. -3. Choose a small, high-signal file bundle with `--files`. +3. Choose a small, high-signal file bundle with `--files`. When remote repository + context would help, use the optional Connected GitHub workflow below. 4. Write the prompt yourself using the Prompt Shape below. 5. Run the smallest useful command, usually `ask-pro --no-temporary --files "" ""` for repo advisories. @@ -88,7 +89,7 @@ again. Example: ## Prompt Shape -Assume Pro has no caller or repository context. Include each material fact and +Assume Pro has no caller context or local checkout. Include each material fact and instruction once: the goal, current state, hard constraints, evidence, success criteria, and required output. Do not rely on the agent's conversation context, repo folklore, prior ask-pro runs, branch names, or unstated user preferences. @@ -113,6 +114,41 @@ Keep bundles focused: source files under review, focused tests, relevant docs, known recent changes, and validation status. Avoid whole-repo bundles unless the question is explicitly architectural. +## Connected GitHub (optional) + +Use ChatGPT's native GitHub app for remote repository evidence when the user +wants it. Reuse an existing connection; do not require setup for file-based +consults. OpenAI documents this app as +[read-only](https://help.openai.com/en/articles/11145903-connecting-github-to-chatgpt). +Availability varies by account, workspace, model, and ChatGPT experience. + +For first setup, direct the human to ChatGPT in their usual browser, using the +same account and workspace as ask-pro, then **Settings > Plugins > GitHub** +(**Apps** on some interfaces). They complete sign-in, review permissions, and +select repositories; organization approval may be needed. Complete setup before +launching ask-pro. Its login recovery automatically continues on sign-in, so +that pause is not a GitHub setup window. Never collect GitHub credentials or +automate authorization. + +If available, GitHub's **Allow read actions** permission can avoid read approval +prompts. The human chooses that setting. It controls approval behavior, not the +underlying permissions of a write-capable app; see +[app permissions](https://help.openai.com/en/articles/20001495-managing-app-permissions-in-chatgpt). + +For the consult, use `--no-temporary` and name the repository (`owner/repo`), +relevant paths, and requested branch or commit in the prompt. Explicitly ask Pro +to use the connected GitHub app, keep the work read-only, cite retrieved files +and the revision actually inspected, and report inaccessible evidence. Do not +infer repository access from ChatGPT login or a successful CLI exit. + +Keep `--files` for local/uncommitted changes and exact snapshots. Distinguish +uploaded evidence from GitHub evidence, and ask Pro to flag revision conflicts. +If the app or requested revision is unavailable, use a focused file bundle and +disclose that limitation. Do not silently switch away from Pro. The CLI submits +text; it does not select an app in the composer or handle app approval prompts. +Until a live run demonstrates retrieval, describe this as an available setup +path, not a verified connection. Confirm material claims against the repo. + ## Output Normal `ask-pro` stdout is compact TOON-style telemetry. Use `state`, `action`, From 3fe9e4ff81a504cc5e16ac6e58681bdc7a3a4171 Mon Sep 17 00:00:00 2001 From: Jonathan Liebig Date: Sat, 10 Oct 2026 00:19:56 +0200 Subject: [PATCH 2/4] Add resumable GitHub connection onboarding --- AGENTS.md | 2 +- CHANGELOG.md | 8 +- README.md | 39 +++++---- bin/ask-pro-cli.ts | 34 +++++++- docs/manual-tests.md | 25 ++++-- docs/windows-work.md | 13 +-- skills/ask-pro/SKILL.md | 36 ++++---- src/ask-pro/browserRunner.ts | 34 +++++++- src/ask-pro/session.ts | 11 ++- src/browser/actions/github.ts | 80 ++++++++++++++++++ src/browser/index.ts | 31 ++++++- src/browser/types.ts | 4 + tests/ask-pro/browserRunner.test.ts | 49 +++++++++++ tests/ask-pro/cli.test.ts | 27 ++++++ tests/browser/github.test.ts | 123 ++++++++++++++++++++++++++++ 15 files changed, 456 insertions(+), 60 deletions(-) create mode 100644 src/browser/actions/github.ts create mode 100644 tests/browser/github.test.ts diff --git a/AGENTS.md b/AGENTS.md index 9c2d1aa2c8..529be3e0b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ ask_pro-specific notes: - Keep the CLI small: `ask-pro ""`, `--files`, `--prompt-file`, `--artifacts` / `--response-zip`, `--dry-run`, `--resume`, `--status`, `--harvest`, `--copy`, `--temporary`, `--no-temporary`, and - `--verbose`. + `--verbose`, and `--github`. - Browser auth is human-controlled. Never ask for, type, read, or log passwords, MFA codes, recovery codes, session cookies, or raw auth tokens. - Browser “Pro thinking” gate: never click or auto-click ChatGPT's `Answer now` diff --git a/CHANGELOG.md b/CHANGELOG.md index e94b86ad68..bcf22f512a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,10 @@ ### Added -- Document optional native ChatGPT GitHub connection and teach the ask-pro skill - to request read-only repository evidence, preserve local-file context, and - report unavailable access. Include an opt-in retrieval smoke; no live GitHub - retrieval has been verified yet. +- Add `--github` with a connection check before upload, human setup in the + managed browser, and a resumable handoff. Request read-only repository evidence + and citations while retaining local-file context. The flag does not restrict + the GitHub plugin's actual permissions. - Add skills CLI installation for Codex and other supported agents using the standalone `ask-pro` npm CLI. - Publish `ask-pro` to npm after each passing main-branch CI run. diff --git a/README.md b/README.md index 38d3f2acf0..1faf527e54 100644 --- a/README.md +++ b/README.md @@ -90,30 +90,35 @@ requires Temporary Chat. The [skill](skills/ask-pro/SKILL.md) has agent guidance ## Optional GitHub context -You can connect GitHub to the ChatGPT account used by ask-pro. OpenAI documents -the native [GitHub app](https://help.openai.com/en/articles/11145903-connecting-github-to-chatgpt) -as read-only. Access depends on the account, workspace, and ChatGPT experience; -a connection alone does not prove it works with ask-pro's selected Pro model. - -Before starting an ask-pro run, open ChatGPT in your usual browser with the same -account and workspace. In **Settings > Plugins** (or **Apps**, if shown), open -GitHub and complete its connection flow yourself. Review permissions and select -only the repositories you want available; an organization may require approval. -Do this separately from ask-pro's login pause, which automatically continues -once ChatGPT sign-in succeeds. +Use `--github` to check the GitHub connection in ask-pro's signed-in ChatGPT +account before sending any prompt or files. It defaults to normal ChatGPT for +recovery; an explicit `--temporary` still requires Temporary Chat. + +If GitHub is disconnected or its connection cannot be confirmed, ask-pro opens +GitHub's ChatGPT page and returns `connect_github_then_resume`. Install the +plugin if needed, connect your account yourself, and review repository access. +Select only the repositories you want available; an organization may require +approval. Then run the emitted `--resume` command. It checks the connection again +before submitting. ChatGPT login alone does not pass this GitHub check. Then explicitly request repository evidence in the prompt: ```powershell -ask-pro --no-temporary "Use the connected GitHub app to read owner/repo. Explain its request authentication flow, cite the files and revision you actually retrieved, and report any access gaps. Do not change anything." +ask-pro --github "Read owner/repo and explain its request authentication flow." ``` Use `--files` for local changes or a precise snapshot that GitHub cannot retrieve. -ask-pro forwards the request; it does not connect the app, select an app in the -composer, approve app actions, or verify that retrieval succeeded. Check the -answer's evidence. The opt-in [GitHub smoke](docs/manual-tests.md#connected-github-context) -describes how to verify the path on your account. GitHub authorization stays with -ChatGPT; ask-pro needs no GitHub token. +The flag requests read-only work and citations, but **does not enforce read-only +GitHub permissions**. The live GitHub plugin advertises write capabilities; +review the actual grant even though the [Help Center](https://help.openai.com/en/articles/11145903-connecting-github-to-chatgpt) +describes a read-only app. [Allow read actions](https://help.openai.com/en/articles/20001495-managing-app-permissions-in-chatgpt) +controls approval prompts, not the underlying access grant. + +A connected account does not prove access to a particular repository, revision, +or Pro experience. ask-pro does not select an app in the composer, approve app +actions, or verify the answer's sources. The opt-in [GitHub smoke](docs/manual-tests.md#connected-github-context) +checks actual retrieval. GitHub authorization stays with ChatGPT; ask-pro needs +no GitHub token. ## Development diff --git a/bin/ask-pro-cli.ts b/bin/ask-pro-cli.ts index 92f95ae9ac..dd4a0f89da 100644 --- a/bin/ask-pro-cli.ts +++ b/bin/ask-pro-cli.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import "dotenv/config"; import { AssistantStoppedError } from "../src/browser/errors.js"; +import { GITHUB_SETUP_URL, GitHubConnectionRequiredError } from "../src/browser/actions/github.js"; import fs from "node:fs/promises"; import path from "node:path"; import { Command, Option } from "commander"; @@ -32,6 +33,7 @@ interface AskProOptions { files?: string[]; promptFile?: string; artifacts?: boolean; + github?: boolean; responseZip?: boolean; resume?: string | boolean; status?: string | boolean; @@ -54,6 +56,14 @@ program .option("--prompt-file ", "read the question from a UTF-8 file; use - for stdin") .option("--artifacts", "ask Pro for ask-pro-response.zip plus markdown fallback") .option("--response-zip", "alias for --artifacts") + .addOption( + new Option("--github", "use connected GitHub; pause for setup before sending").conflicts([ + "resume", + "status", + "harvest", + "copy", + ]), + ) .option("--resume [session-id]", "resume a prepared or waiting ask-pro session") .option("--status [session-id]", "show ask-pro session status") .option("--harvest [session-id]", "print harvested ANSWER.md for a session") @@ -157,6 +167,7 @@ async function runAskPro(question: string, options: AskProOptions): Promise + ask-pro --github --prompt-file github-question.md ``` -4. Compare the answer against the independently recorded file. After the run has +4. With GitHub disconnected or its connection unconfirmed, expect + `NEEDS_GITHUB_CONNECTION`, an opened ChatGPT GitHub setup page, and a resume + command. No prompt or bundle should have been sent. Complete setup manually + after the command exits, then run the printed `--resume` command. Resuming + without completing setup should pause again; completing setup should submit + the original request once. Keep GitHub optional: a new run without `--github` + should still work without a connection. Do not disconnect an existing account + merely to test this path; use a separate profile/account. +5. With GitHub already connected, expect the run to continue without setup. + Both paths use normal ChatGPT by default. Harvest the answer with + `ask-pro --harvest `. +6. Compare the answer against the independently recorded file. After the run has finished, inspect the saved conversation's tool activity for GitHub retrieval evidence. A plausible answer, a public web search, or `COMPLETED` alone is not proof of connected-app use. Record the session, model, repository/ref, and @@ -83,7 +92,7 @@ pass. No GitHub retrieval smoke has been recorded for this workflow yet. If GitHub is unavailable, requires manual app selection/approval, or cannot read the requested revision, record the limitation; the CLI does not automate those -steps. Keep Pro selected and use `--files` for the consult instead. A separate +steps. Keep Pro selected and start a new consult with `--files` instead. A separate manual ChatGPT test does not prove ask-pro can invoke the app. Do not interact with the guarded run window while Pro is answering. diff --git a/docs/windows-work.md b/docs/windows-work.md index 19ff4851d7..bd5ecae043 100644 --- a/docs/windows-work.md +++ b/docs/windows-work.md @@ -28,12 +28,13 @@ Read this when working on `ask_pro` from Windows and add new findings here. follows a replacement DevTools port recorded by the managed profile. Challenge recovery requires a visible active challenge control or surface; queued composer text and class or ID substrings do not trigger it. -- Optional GitHub connection happens in ChatGPT settings before starting a run, - using the same account and workspace as the managed browser. It can be set up - in the user's usual browser; do not copy browser cookies or GitHub tokens. - The managed login recovery automatically resumes on sign-in, so it is not a - pause for app setup. GitHub retrieval through the Pro path still needs the - opt-in check in `docs/manual-tests.md` on the target account. +- `--github` checks the connection in the managed profile after ChatGPT login + and before any upload/submission. Missing or unconfirmed connections restore + Chrome to the GitHub setup page and return `connect_github_then_resume`. + The controller exits before human OAuth; resume rechecks the connection and + starts the unsent prompt, instead of harvesting an unrelated conversation. + The regular Chrome account can differ from ask-pro's account. Do not copy + cookies or GitHub tokens between profiles. - Chrome DevTools state is recorded in each session's `browser.json`; use the saved port for DOM inspection when a live browser needs debugging. - Concurrent fresh and resumed runs on one managed profile use PID-backed diff --git a/skills/ask-pro/SKILL.md b/skills/ask-pro/SKILL.md index 51831a01f8..6c7d2f5822 100644 --- a/skills/ask-pro/SKILL.md +++ b/skills/ask-pro/SKILL.md @@ -116,34 +116,36 @@ question is explicitly architectural. ## Connected GitHub (optional) -Use ChatGPT's native GitHub app for remote repository evidence when the user -wants it. Reuse an existing connection; do not require setup for file-based -consults. OpenAI documents this app as -[read-only](https://help.openai.com/en/articles/11145903-connecting-github-to-chatgpt). -Availability varies by account, workspace, model, and ChatGPT experience. - -For first setup, direct the human to ChatGPT in their usual browser, using the -same account and workspace as ask-pro, then **Settings > Plugins > GitHub** -(**Apps** on some interfaces). They complete sign-in, review permissions, and -select repositories; organization approval may be needed. Complete setup before -launching ask-pro. Its login recovery automatically continues on sign-in, so -that pause is not a GitHub setup window. Never collect GitHub credentials or -automate authorization. +Use `--github` for remote repository evidence when the user wants it. It checks +the connection before uploading or submitting, and defaults to normal ChatGPT. +Do not require GitHub for file-based consults. Availability varies by account, +workspace, model, and ChatGPT experience. + +If the CLI returns `needs_github_connection` / `connect_github_then_resume`, +direct the human to the opened GitHub page in ask-pro's browser. They install the +plugin if needed, complete sign-in, review permissions, and select repositories; +organization approval may be needed. Wait for them to finish, then run the +emitted resume command without adding `--github`. The requirement is saved with +the session and checked again. Do not start a second session or submit through +the browser yourself. Never collect GitHub credentials or automate authorization. If available, GitHub's **Allow read actions** permission can avoid read approval prompts. The human chooses that setting. It controls approval behavior, not the underlying permissions of a write-capable app; see [app permissions](https://help.openai.com/en/articles/20001495-managing-app-permissions-in-chatgpt). +The live GitHub plugin advertises write capabilities despite the Help Center's +read-only description. `--github` requests read-only work; it does not restrict +the provider's permissions or prove that writes are impossible. -For the consult, use `--no-temporary` and name the repository (`owner/repo`), +For the consult, use `--github` and name the repository (`owner/repo`), relevant paths, and requested branch or commit in the prompt. Explicitly ask Pro -to use the connected GitHub app, keep the work read-only, cite retrieved files -and the revision actually inspected, and report inaccessible evidence. Do not +to answer the specific question. The wrapper requests the connected GitHub app, +read-only work, citations, the revision inspected, and access gaps. Do not infer repository access from ChatGPT login or a successful CLI exit. Keep `--files` for local/uncommitted changes and exact snapshots. Distinguish uploaded evidence from GitHub evidence, and ask Pro to flag revision conflicts. -If the app or requested revision is unavailable, use a focused file bundle and +If the app or requested revision is unavailable, offer a new consult with a focused file bundle and disclose that limitation. Do not silently switch away from Pro. The CLI submits text; it does not select an app in the composer or handle app approval prompts. Until a live run demonstrates retrieval, describe this as an available setup diff --git a/src/ask-pro/browserRunner.ts b/src/ask-pro/browserRunner.ts index fa68054a0a..f694760a62 100644 --- a/src/ask-pro/browserRunner.ts +++ b/src/ask-pro/browserRunner.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { DEFAULT_CHATGPT_BROWSER_MODEL_LABEL } from "../browser/chatgptModelCatalog.js"; import { AssistantStoppedError, BrowserAutomationError } from "../browser/errors.js"; +import { GitHubConnectionRequiredError } from "../browser/actions/github.js"; import { askProAgentIdForLegacyBrowserProfileDir, askProAgentIdForManagedBrowserProfileDir, @@ -54,7 +55,7 @@ export async function runAskProBrowserSession( async function runAskProBrowserSessionWithLease({ cwd, sessionId, - temporary, + temporary: requestedTemporary, chatgptUrl: chatgptUrlOverride, browserProfileDir, agentId: agentIdOverride, @@ -64,6 +65,7 @@ async function runAskProBrowserSessionWithLease({ const paths = getAskProSessionPaths(cwd, sessionId); const prompt = await readAskProPrompt({ cwd, sessionId }); const { status: sessionStatus } = await readAskProStatus({ cwd, sessionId }); + const temporary = requestedTemporary ?? (sessionStatus.github ? false : undefined); const artifactsRequested = sessionStatus.artifacts === true; const agentId = agentIdOverride !== undefined ? agentIdOverride : resolveAskProAgentId(); const browserProfile = browserProfileDir ?? (await ensureAskProBrowserProfileDir(agentId)); @@ -106,6 +108,7 @@ async function runAskProBrowserSessionWithLease({ }, ], config: { + github: sessionStatus.github, url: chatgptUrl, manualLogin: true, attachRunning: false, @@ -261,6 +264,17 @@ async function runAskProBrowserSessionWithLease({ }); } + if (error instanceof GitHubConnectionRequiredError) { + await writeTerminalBrowserMetadata(cwd, sessionId, "needs_github_connection", error.reason); + await updateAskProStatus({ + cwd, + sessionId, + status: "NEEDS_GITHUB_CONNECTION", + reason: error.reason, + }); + throw error; + } + if (isAuthGateError(error)) { const currentMetadata = await readBrowserMetadata(paths.browser).catch(() => ({})); await writeAskProBrowserMetadata({ @@ -352,6 +366,21 @@ async function resumeAskProBrowserSessionWithLease({ ? ASK_PRO_CHATGPT_URL : (metadata.url ?? ASK_PRO_TEMPORARY_CHATGPT_URL); const fallbackProfile = await resolveResumeBrowserProfile(metadata); + if ( + sessionStatus.status === "NEEDS_GITHUB_CONNECTION" || + (sessionStatus.github && metadata.runtime?.submissionStarted === false) + ) { + await runAskProBrowserSessionWithLease({ + cwd, + sessionId, + temporary: effectiveTemporary, + browserProfileDir: fallbackProfile, + agentId: metadata.agentId ?? null, + allowStartMinimized: false, + verbose, + }); + return; + } let capturedFinalStatus: FinalAnswerStatus | null = null; if (metadata.runtime) { metadata.runtime = normalizeResumeRuntime( @@ -811,7 +840,7 @@ function authFailureChromeMode(chromeMode: AskProBrowserMetadata["chromeMode"]) async function writeTerminalBrowserMetadata( cwd: string, sessionId: string, - status: "failed" | "wait_timed_out" | "incomplete_answer", + status: "failed" | "wait_timed_out" | "incomplete_answer" | "needs_github_connection", reason: string, ): Promise { const paths = getAskProSessionPaths(cwd, sessionId); @@ -1004,6 +1033,7 @@ interface AskProBrowserMetadata { acceptLanguage?: string; chromeMode?: "launching" | "launched" | "reattaching" | "reused_devtools" | "relaunched"; runtime?: { + submissionStarted?: boolean; chromePid?: number; chromePort?: number; chromeHost?: string; diff --git a/src/ask-pro/session.ts b/src/ask-pro/session.ts index 404421a650..5bfd13e58d 100644 --- a/src/ask-pro/session.ts +++ b/src/ask-pro/session.ts @@ -10,6 +10,7 @@ export type AskProStatus = | "DRY_RUN_COMPLETE" | "READY_TO_SUBMIT" | "NEEDS_USER_AUTH" + | "NEEDS_GITHUB_CONNECTION" | "WAITING" | "WAIT_TIMED_OUT" | "INCOMPLETE_ANSWER" @@ -49,6 +50,7 @@ export interface AskProStatusFile { harvestCommand: string; dryRun: boolean; artifacts?: boolean; + github?: boolean; temporary?: boolean; reason?: string; } @@ -95,12 +97,14 @@ export async function createAskProSession({ filePatterns, dryRun, artifacts = false, + github = false, }: { cwd: string; question: string; filePatterns: string[]; dryRun: boolean; artifacts?: boolean; + github?: boolean; }): Promise { const trimmedQuestion = question.trim(); if (!trimmedQuestion) { @@ -143,9 +147,10 @@ export async function createAskProSession({ harvestCommand: `ask-pro --harvest ${sessionId}`, dryRun, artifacts, + ...(github ? { github: true } : {}), }; - const submittedPrompt = renderSubmittedPrompt(question, artifacts); + const submittedPrompt = renderSubmittedPrompt(question, artifacts, github); const manifestMarkdown = renderManifestMarkdown(manifest); const browserMetadata = { schemaVersion: 1, @@ -885,11 +890,11 @@ function redactSecretsForLog(message: string): string { return redactSecrets(message, "log", findings); } -function renderSubmittedPrompt(question: string, artifacts: boolean): string { +function renderSubmittedPrompt(question: string, artifacts: boolean, github: boolean): string { const artifactRequest = artifacts ? "\nIf file generation is available, also create a downloadable zip named ask-pro-response.zip. It should contain IMPLEMENTATION_PLAN.md, TASKS.json, TEST_PLAN.md, RISK_REGISTER.md, FILES_TO_EDIT.md, and REPO_CONTEXT_USED.md. If you cannot create a zip, return the same content in markdown sections.\n" : ""; - return `${question} + return `${question}${github ? "\n\nUse the connected GitHub app for the requested repository evidence. Keep the work read-only: do not change files, create issues or pull requests, or post comments. Cite the files and revision actually retrieved, and report missing access or revision conflicts." : ""} Read MANIFEST.md in CONTEXT.zip first. Treat the context files it lists as authoritative evidence only for the scope they cover, and call out material gaps or conflicts. ${artifactRequest} diff --git a/src/browser/actions/github.ts b/src/browser/actions/github.ts new file mode 100644 index 0000000000..8dca2d6cce --- /dev/null +++ b/src/browser/actions/github.ts @@ -0,0 +1,80 @@ +import { BrowserAutomationError } from "../errors.js"; +import type { BrowserLogger, ChromeClient } from "../types.js"; +import { delay } from "../utils.js"; + +const GITHUB_PLUGIN_ID = "plugin_connector_1p_1a69035c238881919c4190932b2df699"; +const GITHUB_APP_ID = "connector_76869538009648d5b282a4bb21c3d157"; +export const GITHUB_SETUP_URL = `https://chatgpt.com/plugins/${GITHUB_PLUGIN_ID}`; +const GITHUB_SETTINGS_URL = `https://chatgpt.com/settings/plugins-settings/${GITHUB_PLUGIN_ID}/apps/${GITHUB_APP_ID}`; + +export class GitHubConnectionRequiredError extends BrowserAutomationError { + constructor(readonly reason: "github_connection_required" | "github_connection_unverified") { + super("Connect GitHub in the opened ChatGPT page, review repository access, then resume.", { + stage: "github-connection-required", + }); + } +} + +// Read only visible connection controls, never account identifiers or OAuth state. +function readGitHubConnectionState( + accountSectionId: string, +): "connected" | "disconnected" | "unknown" { + const visible = (element: Element) => { + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + return ( + rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" + ); + }; + const heading = Array.from(document.querySelectorAll("h1")).some( + (element) => visible(element) && element.textContent?.trim() === "GitHub", + ); + if (!heading) return "unknown"; + const section = document.getElementById(accountSectionId); + if (!section || !visible(section)) return "unknown"; + const buttons = Array.from(section.querySelectorAll("button")).filter(visible); + if (buttons.some((button) => /^reconnect\b/i.test(button.textContent?.trim() ?? ""))) { + return "disconnected"; + } + if (buttons.some((button) => button.getAttribute("aria-label")?.startsWith("Actions for "))) { + return "connected"; + } + return buttons.some((button) => /^connect\b/i.test(button.textContent?.trim() ?? "")) + ? "disconnected" + : "unknown"; +} + +export async function ensureGitHubConnection( + Page: ChromeClient["Page"], + Runtime: ChromeClient["Runtime"], + logger: BrowserLogger, + timeoutMs: number, +): Promise { + const { result: location } = await Runtime.evaluate({ + expression: "location.href", + returnByValue: true, + }); + const returnUrl = location.value as string; + await Page.navigate({ url: GITHUB_SETTINGS_URL }); + const deadline = Date.now() + timeoutMs; + let state: "connected" | "disconnected" | "unknown" = "unknown"; + do { + const result = await Runtime.evaluate({ + expression: `(${readGitHubConnectionState.toString()})(${JSON.stringify(`plugin-connected-accounts-${GITHUB_APP_ID}`)})`, + returnByValue: true, + }).catch(() => undefined); + state = result?.result.value ?? "unknown"; + if (state !== "unknown") break; + if (Date.now() < deadline) await delay(300); + } while (Date.now() < deadline); + + if (state !== "connected") { + // Stop on ChatGPT before the human starts OAuth; do not observe consent or callback URLs. + await Page.navigate({ url: GITHUB_SETUP_URL }); + throw new GitHubConnectionRequiredError( + state === "disconnected" ? "github_connection_required" : "github_connection_unverified", + ); + } + logger("GitHub connected. Repository access depends on the selected repositories."); + await Page.navigate({ url: returnUrl }); +} diff --git a/src/browser/index.ts b/src/browser/index.ts index b2a260f88e..bc87dd72fa 100644 --- a/src/browser/index.ts +++ b/src/browser/index.ts @@ -48,6 +48,7 @@ import { import { INPUT_SELECTORS } from "./constants.js"; import { uploadAttachmentViaDataTransfer } from "./actions/remoteFileTransfer.js"; import { ensureThinkingTime } from "./actions/thinkingTime.js"; +import { ensureGitHubConnection, GitHubConnectionRequiredError } from "./actions/github.js"; import { startThinkingStatusMonitor } from "./actions/thinkingStatus.js"; import { createAssistantContinuation } from "./actions/assistantResponse.js"; import { createPostSubmitInputGuard } from "./actions/inputGuard.js"; @@ -450,6 +451,7 @@ export async function runBrowserMode(options: BrowserRunOptions): Promise => { @@ -466,6 +468,7 @@ export async function runBrowserMode(options: BrowserRunOptions): Promise postSubmitInputGuard.enable() : undefined, }; + submissionStarted = true; + await emitRuntimeHint(); await raceWithDisconnect( runProviderSubmissionFlow(chatgptDomProvider, { prompt, @@ -1612,6 +1623,12 @@ export async function runBrowserMode(options: BrowserRunOptions): Promise { @@ -2274,6 +2293,7 @@ async function runRemoteBrowserMode( tabUrl: lastUrl, conversationId, controllerPid: process.pid, + submissionStarted, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); @@ -2435,6 +2455,12 @@ async function runRemoteBrowserMode( await raceWithDisconnect(ensureLoggedIn(Runtime, logger, { remoteSession: true })); await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger)); } + if (config.github) { + await raceWithDisconnect( + ensureGitHubConnection(Page, Runtime, logger, config.inputTimeoutMs), + ); + await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger)); + } logger( `Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`, ); @@ -2532,6 +2558,8 @@ async function runRemoteBrowserMode( attachmentNames, afterSubmit: postSubmitInputGuard ? () => postSubmitInputGuard.enable() : undefined, }; + submissionStarted = true; + await emitRuntimeHint(); await runProviderSubmissionFlow(chatgptDomProvider, { prompt, evaluate: async () => undefined, @@ -2904,6 +2932,7 @@ async function runRemoteBrowserMode( }; } catch (error) { const normalizedError = error instanceof Error ? error : new Error(String(error)); + githubSetupRequired = normalizedError instanceof GitHubConnectionRequiredError; const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError); connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed; @@ -2936,7 +2965,7 @@ async function runRemoteBrowserMode( // ignore } removeDialogHandler?.(); - if (ownsTarget) { + if (ownsTarget && !githubSetupRequired) { await closeRemoteChromeTarget(host, port, remoteTargetId ?? undefined, logger); } // Don't kill remote Chrome - it's not ours to manage diff --git a/src/browser/types.ts b/src/browser/types.ts index 4c7c337633..40a5910ce9 100644 --- a/src/browser/types.ts +++ b/src/browser/types.ts @@ -18,6 +18,7 @@ export interface BrowserAttachment { } export interface BrowserRuntimeMetadata { + submissionStarted?: boolean; browserTransport?: "cdp"; chromePid?: number; chromePort?: number; @@ -32,6 +33,7 @@ export interface BrowserRuntimeMetadata { } export interface BrowserAutomationConfig { + github?: boolean; chromeProfile?: string | null; chromePath?: string | null; chromeCookiePath?: string | null; @@ -142,8 +144,10 @@ export type ResolvedBrowserConfig = Required< | "acceptLanguage" | "thinkingTime" | "modelStrategy" + | "github" > > & { + github?: boolean; chromeProfile?: string | null; chromePath?: string | null; chromeCookiePath?: string | null; diff --git a/tests/ask-pro/browserRunner.test.ts b/tests/ask-pro/browserRunner.test.ts index d461913ca5..1a18d5ece6 100644 --- a/tests/ask-pro/browserRunner.test.ts +++ b/tests/ask-pro/browserRunner.test.ts @@ -1,4 +1,6 @@ import { AssistantStoppedError } from "../../src/browser/errors.js"; +import { BrowserAutomationError } from "../../src/browser/errors.js"; +import { GitHubConnectionRequiredError } from "../../src/browser/actions/github.js"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -143,6 +145,53 @@ afterEach(async () => { }); describe("ask-pro browser runner", () => { + test.each(["github", "login"])( + "resumes a GitHub request after a %s handoff without harvesting an unsent prompt", + async (gate) => { + const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "ask-pro-github-handoff-")); + tempDirs.push(cwd); + const session = await createAskProSession({ + cwd, + question: "Read the public repository.", + filePatterns: [], + dryRun: false, + github: true, + }); + const failure = + gate === "github" + ? new GitHubConnectionRequiredError("github_connection_required") + : new BrowserAutomationError("ChatGPT login required", { stage: "login-required" }); + const pause = async (options: { runtimeHintCb: (hint: unknown) => Promise }) => { + await options.runtimeHintCb({ + chromePort: 9224, + submissionStarted: false, + tabUrl: "https://chatgpt.com/", + }); + throw failure; + }; + runBrowserModeMock.mockImplementationOnce(pause as never); + await expect(runAskProBrowserSession({ cwd, sessionId: session.id })).rejects.toThrow(); + expect((await readAskProStatus({ cwd, sessionId: session.id })).status.status).toBe( + gate === "github" ? "NEEDS_GITHUB_CONNECTION" : "NEEDS_USER_AUTH", + ); + runBrowserModeMock.mockImplementationOnce(pause as never); + await expect(resumeAskProBrowserSession({ cwd, sessionId: session.id })).rejects.toThrow(); + runBrowserModeMock.mockImplementationOnce(async (...args: unknown[]) => { + expect(args[0]).toMatchObject({ config: { github: true, url: "https://chatgpt.com/" } }); + return { + answerText: "New GitHub answer", + answerMarkdown: "# New GitHub answer\n", + browserTransport: "launched", + }; + }); + await resumeAskProBrowserSession({ cwd, sessionId: session.id }); + expect((await readAskProAnswer({ cwd, sessionId: session.id })).answer).toBe( + "# New GitHub answer\n", + ); + expect(resumeBrowserSessionMock).not.toHaveBeenCalled(); + }, + ); + test("rejects a live same-session controller and releases the lease in finally", async () => { const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "ask-pro-controller-live-")); tempDirs.push(cwd); diff --git a/tests/ask-pro/cli.test.ts b/tests/ask-pro/cli.test.ts index 1b4f75f12a..c72f365e69 100644 --- a/tests/ask-pro/cli.test.ts +++ b/tests/ask-pro/cli.test.ts @@ -27,6 +27,33 @@ afterEach(async () => { }); describe("ask-pro cli", () => { + test("keeps GitHub required across preparation and exposes a resumable setup handoff", async () => { + const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "ask-pro-cli-github-")); + tempDirs.push(cwd); + const cli = path.join(process.cwd(), "bin", "ask-pro-cli.ts"); + const tsxLoader = pathToFileURL( + path.join(process.cwd(), "node_modules", "tsx", "dist", "esm", "index.mjs"), + ).href; + const invoke = (...args: string[]) => + execFileAsync(process.execPath, ["--import", tsxLoader, cli, ...args], { cwd }); + const prepared = await invoke("--github", "--dry-run", "Read owner/repo."); + expect(prepared.stdout).toContain(" temporary: off\n"); + const [id] = await fs.readdir(path.join(cwd, ".ask-pro", "sessions")); + const statusPath = path.join(cwd, ".ask-pro", "sessions", id!, "status.json"); + const status = JSON.parse(await fs.readFile(statusPath, "utf8")); + expect(status).toMatchObject({ github: true, temporary: false, status: "DRY_RUN_COMPLETE" }); + await fs.writeFile( + statusPath, + JSON.stringify({ ...status, status: "NEEDS_GITHUB_CONNECTION" }), + ); + const paused = await invoke("--status", id!); + expect(paused.stdout).toContain(" action: connect_github_then_resume\n"); + expect(paused.stdout).toContain(`--resume ${id}`); + const harvest = await invoke("--harvest", id!); + expect(harvest.stdout).toContain(" state: needs_github_connection\n"); + expect(harvest.stdout).not.toContain("# Dry Run"); + }, 30000); + test("documents the V1 switches", async () => { const cli = path.join(process.cwd(), "bin", "ask-pro-cli.ts"); const tsxLoader = pathToFileURL( diff --git a/tests/browser/github.test.ts b/tests/browser/github.test.ts new file mode 100644 index 0000000000..68e46f9cef --- /dev/null +++ b/tests/browser/github.test.ts @@ -0,0 +1,123 @@ +import { runInNewContext } from "node:vm"; +import { describe, expect, test, vi } from "vitest"; +import { + ensureGitHubConnection, + GitHubConnectionRequiredError, + GITHUB_SETUP_URL, +} from "../../src/browser/actions/github.js"; + +function element(textContent: string, label = "", hidden = false) { + return { + textContent, + getAttribute: () => label, + getBoundingClientRect: () => ({ width: hidden ? 0 : 100, height: 30 }), + click: () => { + throw new Error("Connection checks must not click consent"); + }, + }; +} + +describe("GitHub connection preflight", () => { + test.each([ + { + name: "a connected GitHub account", + heading: "GitHub", + ownSection: true, + account: true, + hidden: false, + reconnect: false, + connected: true, + }, + { + name: "no connected account", + heading: "GitHub", + ownSection: true, + account: false, + hidden: false, + reconnect: false, + connected: false, + }, + { + name: "another app's account", + heading: "GitHub", + ownSection: false, + account: true, + hidden: false, + reconnect: false, + connected: false, + }, + { + name: "an expired connection", + heading: "GitHub", + ownSection: true, + account: true, + hidden: false, + reconnect: true, + connected: false, + }, + { + name: "a hidden account control", + heading: "GitHub", + ownSection: true, + account: true, + hidden: true, + reconnect: false, + connected: false, + }, + { + name: "an unavailable app", + heading: "Not found", + ownSection: false, + account: false, + hidden: false, + reconnect: false, + connected: false, + }, + ])("checks $name without handling authorization", async (fixture) => { + const buttons = [ + element("Connect another account"), + ...(fixture.account ? [element("", "Actions for test account", fixture.hidden)] : []), + ...(fixture.reconnect ? [element("Reconnect")] : []), + ]; + const section = { ...element(""), querySelectorAll: () => buttons }; + const document = { + querySelectorAll: () => [element(fixture.heading)], + getElementById: (id: string) => + fixture.ownSection && + id === "plugin-connected-accounts-connector_76869538009648d5b282a4bb21c3d157" + ? section + : null, + get cookie() { + throw new Error("Must not read credentials"); + }, + }; + const runtime = { + evaluate: vi.fn(async ({ expression }: { expression: string }) => ({ + result: { + value: + expression === "location.href" + ? "https://chatgpt.com/?temporary-chat=true" + : runInNewContext(expression, { + document, + getComputedStyle: () => ({ display: "block", visibility: "visible" }), + }), + }, + })), + }; + const destinations: string[] = []; + const page = { + navigate: async ({ url }: { url: string }) => { + destinations.push(url); + }, + }; + const pending = ensureGitHubConnection(page as never, runtime as never, () => {}, 0); + if (fixture.connected) { + await pending; + expect(destinations.at(-1)).toBe("https://chatgpt.com/?temporary-chat=true"); + } else { + await expect(pending).rejects.toBeInstanceOf(GitHubConnectionRequiredError); + expect(destinations.at(-1)).toBe(GITHUB_SETUP_URL); + } + expect(destinations.every((url) => new URL(url).origin === "https://chatgpt.com")).toBe(true); + }); +}); From 6bb682ea7f7d91f96affd7eac6007f1c074f7496 Mon Sep 17 00:00:00 2001 From: Jonathan Liebig Date: Sat, 10 Oct 2026 00:26:08 +0200 Subject: [PATCH 3/4] Prefer GitHub evidence and scope retries to pending setup --- README.md | 2 ++ skills/ask-pro/SKILL.md | 24 +++++++++++++++--------- src/ask-pro/browserRunner.ts | 9 +++++---- src/browser/index.ts | 27 +++++++++++++++++---------- src/browser/types.ts | 2 +- tests/ask-pro/browserRunner.test.ts | 28 ++++++++++++++++++++++++++-- 6 files changed, 66 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 1faf527e54..cb590850f0 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ ask-pro --github "Read owner/repo and explain its request authentication flow." ``` Use `--files` for local changes or a precise snapshot that GitHub cannot retrieve. +For committed repository code, prefer `--github` with the repository, ref, paths, +and question; source-file uploads are usually unnecessary when retrieval works. The flag requests read-only work and citations, but **does not enforce read-only GitHub permissions**. The live GitHub plugin advertises write capabilities; review the actual grant even though the [Help Center](https://help.openai.com/en/articles/11145903-connecting-github-to-chatgpt) diff --git a/skills/ask-pro/SKILL.md b/skills/ask-pro/SKILL.md index 6c7d2f5822..5bd9234084 100644 --- a/skills/ask-pro/SKILL.md +++ b/skills/ask-pro/SKILL.md @@ -34,11 +34,13 @@ When invoked: 1. Inspect the repo and the relevant files. 2. Identify the exact decision Pro should answer. -3. Choose a small, high-signal file bundle with `--files`. When remote repository - context would help, use the optional Connected GitHub workflow below. +3. For committed code on GitHub, prefer `--github` and give Pro the repository, + ref, relevant paths, and question. Let it retrieve the code instead of assembling + a source bundle. Add a small `--files` bundle only for uncommitted changes, + local-only evidence, or repository content the connection cannot access. 4. Write the prompt yourself using the Prompt Shape below. -5. Run the smallest useful command, usually - `ask-pro --no-temporary --files "" ""` for repo advisories. +5. Run the smallest useful command: `ask-pro --github ""` for GitHub + evidence, or `ask-pro --no-temporary --files "" ""` for a file bundle. For multiline prompts, write a temporary prompt file and use `ask-pro --no-temporary --prompt-file --files ""`; do not rely on shell multiline quoting. @@ -116,7 +118,7 @@ question is explicitly architectural. ## Connected GitHub (optional) -Use `--github` for remote repository evidence when the user wants it. It checks +Prefer `--github` for remote repository evidence. It checks the connection before uploading or submitting, and defaults to normal ChatGPT. Do not require GitHub for file-based consults. Availability varies by account, workspace, model, and ChatGPT experience. @@ -137,15 +139,18 @@ The live GitHub plugin advertises write capabilities despite the Help Center's read-only description. `--github` requests read-only work; it does not restrict the provider's permissions or prove that writes are impossible. -For the consult, use `--github` and name the repository (`owner/repo`), -relevant paths, and requested branch or commit in the prompt. Explicitly ask Pro +If GitHub is already connected, proceed without a setup question or source-file +bundle. For the consult, name the repository (`owner/repo`), relevant paths, and +requested branch or commit in the prompt. Explicitly ask Pro to answer the specific question. The wrapper requests the connected GitHub app, read-only work, citations, the revision inspected, and access gaps. Do not infer repository access from ChatGPT login or a successful CLI exit. -Keep `--files` for local/uncommitted changes and exact snapshots. Distinguish +Keep `--files` for local/uncommitted changes, non-GitHub evidence, and snapshots +that the app cannot retrieve. Distinguish uploaded evidence from GitHub evidence, and ask Pro to flag revision conflicts. -If the app or requested revision is unavailable, offer a new consult with a focused file bundle and +If the app or requested revision is unavailable, offer a new consult with a +focused file bundle and disclose that limitation. Do not silently switch away from Pro. The CLI submits text; it does not select an app in the composer or handle app approval prompts. Until a live run demonstrates retrieval, describe this as an available setup @@ -186,6 +191,7 @@ state is `INCOMPLETE_ANSWER` / `preamble_without_artifacts`, do not treat ```bash ask-pro "Review the async billing webhook migration plan and return an implementation plan." ask-pro --no-temporary --prompt-file question.md --files src --files tests +ask-pro --github "In owner/repo at , inspect src/auth and explain token validation." ask-pro --temporary "Review this sensitive migration plan, and fail if Temporary Chat cannot use Pro." ask-pro --no-temporary "Review this in normal ChatGPT instead of Temporary Chat." ask-pro --prompt-file question.md --files .\src diff --git a/src/ask-pro/browserRunner.ts b/src/ask-pro/browserRunner.ts index f694760a62..aa97d69ba9 100644 --- a/src/ask-pro/browserRunner.ts +++ b/src/ask-pro/browserRunner.ts @@ -218,7 +218,7 @@ async function runAskProBrowserSessionWithLease({ sessionId, "Temporary Chat did not expose the Pro model; retrying in normal ChatGPT.", ); - await closeFallbackTemporaryTab(cwd, sessionId, logger); + await closeSessionBrowserTab(cwd, sessionId, logger); await updateAskProStatus({ cwd, sessionId, @@ -368,8 +368,9 @@ async function resumeAskProBrowserSessionWithLease({ const fallbackProfile = await resolveResumeBrowserProfile(metadata); if ( sessionStatus.status === "NEEDS_GITHUB_CONNECTION" || - (sessionStatus.github && metadata.runtime?.submissionStarted === false) + (sessionStatus.github && metadata.runtime?.githubCheckPending === true) ) { + await closeSessionBrowserTab(cwd, sessionId, logger); await runAskProBrowserSessionWithLease({ cwd, sessionId, @@ -619,7 +620,7 @@ function withNoTemporaryResumeCommand(command: string): string { return withoutStrictTemporary.replace(/\s--resume(?=\s|$)/, " --no-temporary --resume"); } -async function closeFallbackTemporaryTab( +async function closeSessionBrowserTab( cwd: string, sessionId: string, logger: BrowserLogger, @@ -1033,7 +1034,7 @@ interface AskProBrowserMetadata { acceptLanguage?: string; chromeMode?: "launching" | "launched" | "reattaching" | "reused_devtools" | "relaunched"; runtime?: { - submissionStarted?: boolean; + githubCheckPending?: boolean; chromePid?: number; chromePort?: number; chromeHost?: string; diff --git a/src/browser/index.ts b/src/browser/index.ts index bc87dd72fa..90c8f0e0ca 100644 --- a/src/browser/index.ts +++ b/src/browser/index.ts @@ -451,10 +451,10 @@ export async function runBrowserMode(options: BrowserRunOptions): Promise => { + const emitRuntimeHint = async (required = false): Promise => { if (!runtimeHintCb || !chrome?.port) { return; } @@ -468,11 +468,12 @@ export async function runBrowserMode(options: BrowserRunOptions): Promise postSubmitInputGuard.enable() : undefined, }; - submissionStarted = true; - await emitRuntimeHint(); await raceWithDisconnect( runProviderSubmissionFlow(chatgptDomProvider, { prompt, @@ -1719,6 +1722,9 @@ export async function runBrowserMode(options: BrowserRunOptions): Promise { + const emitRuntimeHint = async (required = false) => { if (!runtimeHintCb) return; try { const conversationId = lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined; @@ -2293,9 +2299,10 @@ async function runRemoteBrowserMode( tabUrl: lastUrl, conversationId, controllerPid: process.pid, - submissionStarted, + githubCheckPending, }); } catch (error) { + if (required) throw error; const message = error instanceof Error ? error.message : String(error); logger(`Failed to persist runtime hint: ${message}`); } @@ -2459,6 +2466,8 @@ async function runRemoteBrowserMode( await raceWithDisconnect( ensureGitHubConnection(Page, Runtime, logger, config.inputTimeoutMs), ); + githubCheckPending = false; + await emitRuntimeHint(true); await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger)); } logger( @@ -2558,8 +2567,6 @@ async function runRemoteBrowserMode( attachmentNames, afterSubmit: postSubmitInputGuard ? () => postSubmitInputGuard.enable() : undefined, }; - submissionStarted = true; - await emitRuntimeHint(); await runProviderSubmissionFlow(chatgptDomProvider, { prompt, evaluate: async () => undefined, diff --git a/src/browser/types.ts b/src/browser/types.ts index 40a5910ce9..c9a8576be0 100644 --- a/src/browser/types.ts +++ b/src/browser/types.ts @@ -18,7 +18,7 @@ export interface BrowserAttachment { } export interface BrowserRuntimeMetadata { - submissionStarted?: boolean; + githubCheckPending?: boolean; browserTransport?: "cdp"; chromePid?: number; chromePort?: number; diff --git a/tests/ask-pro/browserRunner.test.ts b/tests/ask-pro/browserRunner.test.ts index 1a18d5ece6..667e9832ba 100644 --- a/tests/ask-pro/browserRunner.test.ts +++ b/tests/ask-pro/browserRunner.test.ts @@ -146,7 +146,7 @@ afterEach(async () => { describe("ask-pro browser runner", () => { test.each(["github", "login"])( - "resumes a GitHub request after a %s handoff without harvesting an unsent prompt", + "resumes a GitHub request after a %s handoff before its connection check completes", async (gate) => { const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "ask-pro-github-handoff-")); tempDirs.push(cwd); @@ -164,7 +164,8 @@ describe("ask-pro browser runner", () => { const pause = async (options: { runtimeHintCb: (hint: unknown) => Promise }) => { await options.runtimeHintCb({ chromePort: 9224, - submissionStarted: false, + chromeTargetId: "github-setup-target", + githubCheckPending: true, tabUrl: "https://chatgpt.com/", }); throw failure; @@ -176,6 +177,12 @@ describe("ask-pro browser runner", () => { ); runBrowserModeMock.mockImplementationOnce(pause as never); await expect(resumeAskProBrowserSession({ cwd, sessionId: session.id })).rejects.toThrow(); + expect(closeTabMock).toHaveBeenLastCalledWith( + 9224, + "github-setup-target", + expect.any(Function), + "127.0.0.1", + ); runBrowserModeMock.mockImplementationOnce(async (...args: unknown[]) => { expect(args[0]).toMatchObject({ config: { github: true, url: "https://chatgpt.com/" } }); return { @@ -189,6 +196,23 @@ describe("ask-pro browser runner", () => { "# New GitHub answer\n", ); expect(resumeBrowserSessionMock).not.toHaveBeenCalled(); + + await updateAskProStatus({ cwd, sessionId: session.id, status: "WAITING" }); + await writeAskProBrowserMetadata({ + cwd, + sessionId: session.id, + metadata: { + profileDir: testSharedProfileDir(), + temporary: false, + runtime: { chromePort: 9224, githubCheckPending: false }, + }, + }); + runBrowserModeMock.mockClear(); + closeTabMock.mockClear(); + await resumeAskProBrowserSession({ cwd, sessionId: session.id }); + expect(resumeBrowserSessionMock).toHaveBeenCalledTimes(1); + expect(runBrowserModeMock).not.toHaveBeenCalled(); + expect(closeTabMock).not.toHaveBeenCalled(); }, ); From b9506ede43b550f463343f1ed1492c9442f26ec9 Mon Sep 17 00:00:00 2001 From: Jonathan Liebig Date: Sat, 10 Oct 2026 00:38:02 +0200 Subject: [PATCH 4/4] Record GitHub onboarding dogfood evidence and limits --- docs/manual-tests.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/manual-tests.md b/docs/manual-tests.md index 014e5cbc1b..ce4d0ceba5 100644 --- a/docs/manual-tests.md +++ b/docs/manual-tests.md @@ -168,6 +168,19 @@ should report `responseZip.status = "not_requested"`. ## Recent Smoke Runs +- 2026-10-10 - GitHub onboarding trial + `2026-10-09T221451-return-a-short-markdown-answer-with-no-preamble--3f717c89` + paused with `NEEDS_GITHUB_CONNECTION` before upload or submission when the + connection UI was unconfirmed. After human setup, `--resume` passed the check, + selected Pro, and returned the correct migration marker and SHA-256/10-character + hash details from `src/browser/profilePaths.ts` at + `a673c37480ee516d14e8d1f7c395611ae49244a2`. The bundle contained no source files. + The answer reported GitHub file retrieval and the requested revision, but its + citation link was absent from the harvested Markdown and the activity panel + was not independently inspected. This proves the setup handoff/resume and + answer accuracy, not a verified tool trace or a definitively disconnected + initial account. Final recovery-only fixes were covered by the owning tests + and CI after this live run started. - 2026-07-10 - `2026-07-10T145226-return-exactly-one-line-and-nothing-else-ask-pro-d95f47cc` selected `GPT-5.6 Sol`, confirmed `Pro` intelligence, uploaded the README context bundle, harvested `ASK_PRO_GPT56_SOL_PRO_OK`, and completed browser