ask-pro 0.1.69 → 0.1.71

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/README.md CHANGED
@@ -6,39 +6,47 @@ and owns the result; ask-pro never applies generated code.
6
6
 
7
7
  ## Install
8
8
 
9
- ```powershell
10
- npx skills add JJLiebig/ask-pro -g
11
- ```
12
-
13
- This installs the `ask-pro` skill for Codex, Claude Code, and the other agents
14
- the [skills CLI](https://github.com/vercel-labs/skills) supports. The skill
15
- contains ask-pro itself; its first run installs dependencies and builds, so no
16
- separate CLI install is needed. Update with `npx skills update`.
9
+ Both options need Node.js 24+, Chrome, and a ChatGPT account with Pro access.
10
+ Browser login is manual.
17
11
 
18
- Codex users can install it as a plugin instead:
12
+ ### Codex plugin marketplace
19
13
 
20
14
  ```powershell
21
15
  codex plugin marketplace add JJLiebig/ask-pro
22
16
  codex plugin add ask-pro@ask-pro
23
17
  ```
24
18
 
25
- Update with `codex plugin marketplace upgrade ask-pro`, then restart Codex.
26
- Both installs run the same bundled launcher and share the same state.
19
+ This route needs Git and npm. The plugin's cached runner downloads pnpm, builds
20
+ outside the plugin cache, and keeps the installed snapshot unchanged. If
21
+ `ask-pro` is not on `PATH`, the skill uses that runner.
27
22
 
28
- You need Node.js 24+, npm, Chrome, and a ChatGPT account with Pro access. The
29
- browser runtime is developed and tested on Windows; other operating systems
30
- need validation.
23
+ Update with `codex plugin marketplace upgrade ask-pro`, then restart Codex to
24
+ load the updated skill.
25
+
26
+ ### Skills CLI (Codex and other supported agents)
27
+
28
+ Install the skill and its standalone CLI separately:
31
29
 
32
- **Upgrading from an earlier version:** ask-pro state moved from
33
- `~/.codex/state/ask-pro` to `~/.agents/state/ask-pro`. Before the first run
34
- after upgrading, and while no ask-pro run is active, copy your signed-in browser
35
- state once:
30
+ ```powershell
31
+ npx skills add JJLiebig/ask-pro
32
+ npm install --global ask-pro
33
+ ```
34
+
35
+ The [skills CLI](https://github.com/vercel-labs/skills) installs the skill in
36
+ the current project by default. Add `-g` for a user-wide skill or `-a codex`
37
+ to select Codex; it supports other agents too. The npm CLI runs without Codex's
38
+ plugin cache.
39
+
40
+ Update both parts:
36
41
 
37
42
  ```powershell
38
- New-Item -ItemType Directory -Force "$HOME\.agents\state" | Out-Null
39
- Copy-Item -Recurse "$HOME\.codex\state\ask-pro" "$HOME\.agents\state\ask-pro"
43
+ npx skills update
44
+ npm update --global ask-pro
40
45
  ```
41
46
 
47
+ The browser runtime is developed and tested on Windows. Other operating systems
48
+ need validation.
49
+
42
50
  ## Browser and session state
43
51
 
44
52
  ask-pro never asks for, types, reads, or logs passwords, MFA codes, recovery
@@ -46,9 +54,10 @@ codes, session cookies, or raw auth tokens. A new browser profile may need one
46
54
  manual ChatGPT login. On Windows, ordinary managed Chrome runs start minimized
47
55
  and are hidden; login and recovery restore the window for human action.
48
56
 
49
- ask-pro keeps its state in `~/.agents/state/ask-pro/`: the persistent browser
50
- profile in `browser-profile/` and the builds of the installed skill in
51
- `runtimes/`. Set `ASK_PRO_STATE_DIR` to use a different folder.
57
+ The persistent profile lives at `$CODEX_HOME/state/ask-pro/browser-profile`, or
58
+ `~/.codex/state/ask-pro/browser-profile` when `CODEX_HOME` is unset. Existing
59
+ legacy profiles are migrated on first use; see [Windows notes](docs/windows-work.md)
60
+ for copy and recovery details.
52
61
 
53
62
  Project sessions live under `.ask-pro/sessions/<id>/`. Add `.ask-pro/` to your
54
63
  project's `.gitignore`. At each invocation, ask-pro deletes entire session
@@ -56,9 +65,7 @@ directories at least seven days old, regardless of state.
56
65
 
57
66
  ## Use
58
67
 
59
- Agents run ask-pro through the installed skill, as
60
- `node <skill-folder>/scripts/ask-pro.mjs`; `ask-pro` below stands for that
61
- command. Ask for a recoverable repo consult:
68
+ Ask for a recoverable repo consult:
62
69
 
63
70
  ```powershell
64
71
  ask-pro --no-temporary --prompt-file question.md --files src --files tests
@@ -79,7 +86,7 @@ ask-pro --harvest <session-id>
79
86
 
80
87
  Use `ask-pro --help` for the full CLI. For multiline prompts, use
81
88
  `--prompt-file`. Prefer `--no-temporary` when recovery matters; `--temporary`
82
- requires Temporary Chat. The [skill](SKILL.md) has agent guidance.
89
+ requires Temporary Chat. The [skill](skills/ask-pro/SKILL.md) has agent guidance.
83
90
 
84
91
  When Pro asks native questions, ask-pro returns `awaiting_answers`, prints each
85
92
  question and its numbered choices, and saves `QUESTIONS.json`. The calling agent
@@ -147,8 +154,8 @@ pnpm run format:check
147
154
  pnpm pack --dry-run
148
155
  ```
149
156
 
150
- To try local changes as an installed skill, run
151
- `npx skills add <path-to-checkout> -g`. Live browser tests are opt-in; see
157
+ After changing plugin files, run `pnpm run plugin:refresh` and restart Codex.
158
+ Do not edit the installed plugin cache. Live browser tests are opt-in; see
152
159
  [manual tests](docs/manual-tests.md).
153
160
 
154
161
  After the build matrix passes on `main`, [CI](.github/workflows/ci.yml) publishes
@@ -5,7 +5,7 @@ import { AssistantStoppedError, BrowserAutomationError } from "../browser/errors
5
5
  import { AssistantQuestionsError } from "../browser/actions/questions.js";
6
6
  import { readAskProQuestions, parseQuestionAnswers, writeAskProQuestions } from "./questions.js";
7
7
  import { GitHubConnectionRequiredError } from "../browser/actions/github.js";
8
- import { askProAgentIdForManagedBrowserProfileDir, askProBrowserProfileDirForAgentId, isAskProManagedBrowserProfileDir, resolveAskProAgentId, } from "../browser/profilePaths.js";
8
+ import { askProAgentIdForLegacyBrowserProfileDir, askProAgentIdForManagedBrowserProfileDir, askProBrowserProfileDirForAgentId, ensureAskProBrowserProfileDir, isLegacyAskProManagedBrowserProfileDir, isAskProManagedBrowserProfileDir, resolveAskProAgentId, } from "../browser/profilePaths.js";
9
9
  import { runBrowserMode } from "../browserMode.js";
10
10
  import { closeTab } from "../browser/chromeLifecycle.js";
11
11
  import { resumeBrowserSession } from "../browser/reattach.js";
@@ -28,7 +28,7 @@ async function runAskProBrowserSessionWithLease({ cwd, sessionId, temporary: req
28
28
  const temporary = requestedTemporary ?? (sessionStatus.github ? false : undefined);
29
29
  const artifactsRequested = sessionStatus.artifacts === true;
30
30
  const agentId = agentIdOverride !== undefined ? agentIdOverride : resolveAskProAgentId();
31
- const browserProfile = browserProfileDir ?? askProBrowserProfileDirForAgentId(agentId);
31
+ const browserProfile = browserProfileDir ?? (await ensureAskProBrowserProfileDir(agentId));
32
32
  const metadata = await readBrowserMetadata(paths.browser).catch(() => null);
33
33
  const chatgptUrl = chatgptUrlOverride ??
34
34
  (temporary === true
@@ -317,7 +317,7 @@ async function resumeAskProBrowserSessionWithLease({ cwd, sessionId, temporary,
317
317
  : effectiveTemporary === false
318
318
  ? ASK_PRO_CHATGPT_URL
319
319
  : (metadata.url ?? ASK_PRO_TEMPORARY_CHATGPT_URL);
320
- const fallbackProfile = resolveResumeBrowserProfile(metadata);
320
+ const fallbackProfile = await resolveResumeBrowserProfile(metadata);
321
321
  if (sessionStatus.status === "NEEDS_GITHUB_CONNECTION" ||
322
322
  (sessionStatus.github && metadata.runtime?.githubCheckPending === true)) {
323
323
  await closeSessionBrowserTab(cwd, sessionId, logger);
@@ -716,26 +716,41 @@ async function writeTerminalBrowserMetadata(cwd, sessionId, status, reason) {
716
716
  },
717
717
  });
718
718
  }
719
- function resolveResumeBrowserProfile(metadata) {
719
+ async function resolveResumeBrowserProfile(metadata) {
720
720
  const agentProfile = resolveStoredAgentProfile(metadata.agentId);
721
721
  const profileDir = metadata.profileDir;
722
722
  const profileAgentId = profileDir ? askProAgentIdForManagedBrowserProfileDir(profileDir) : null;
723
+ const legacyAgentId = profileDir ? askProAgentIdForLegacyBrowserProfileDir(profileDir) : null;
723
724
  if (profileAgentId && profileAgentId !== metadata.agentId) {
724
725
  throw new Error("Stored ask-pro agent profile does not match stored agent id.");
725
726
  }
727
+ if (legacyAgentId && legacyAgentId !== metadata.agentId) {
728
+ throw new Error("Stored ask-pro agent profile does not match stored agent id.");
729
+ }
726
730
  if (profileAgentId && agentProfile) {
727
731
  return profileDir;
728
732
  }
733
+ if (legacyAgentId && agentProfile) {
734
+ return ensureAskProBrowserProfileDir(metadata.agentId);
735
+ }
729
736
  if (profileDir &&
730
737
  !profileAgentId &&
738
+ !legacyAgentId &&
731
739
  !agentProfile &&
732
740
  isAskProManagedBrowserProfileDir(profileDir)) {
733
- return askProBrowserProfileDirForAgentId(null);
741
+ return ensureAskProBrowserProfileDir(null);
742
+ }
743
+ if (profileDir &&
744
+ !profileAgentId &&
745
+ !legacyAgentId &&
746
+ !agentProfile &&
747
+ isLegacyAskProManagedBrowserProfileDir(profileDir)) {
748
+ return ensureAskProBrowserProfileDir(null);
734
749
  }
735
750
  if (profileDir) {
736
751
  throw new Error("Stored ask-pro profile path is invalid.");
737
752
  }
738
- return askProBrowserProfileDirForAgentId(metadata.agentId);
753
+ return ensureAskProBrowserProfileDir(metadata.agentId);
739
754
  }
740
755
  function resolveStoredAgentProfile(agentId) {
741
756
  if (!agentId)
@@ -1,13 +1,144 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import fs from "node:fs/promises";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
5
+ import { acquireProfileRunLock, isBrowserProfileInUse, isProcessAlive, releaseProfileRunLock, } from "./profileState.js";
4
6
  const RESOLVED_AGENT_ID_PATTERN = /^[a-z0-9._-]+-[a-f0-9]{10}$/;
7
+ const MIGRATION_MARKER = ".ask-pro-profile-migration";
5
8
  export function defaultAskProBrowserProfileDir() {
6
9
  return askProBrowserProfileDirForAgentId(null);
7
10
  }
8
11
  export function askProBrowserProfileDirForAgentId(agentId, env = process.env) {
9
12
  return profileDirUnder(askProStateDir(env), agentId);
10
13
  }
14
+ export function legacyAskProBrowserProfileDirForAgentId(agentId, homeDir = os.homedir()) {
15
+ return profileDirUnder(path.join(homeDir, ".agents", "skills", "ask-pro"), agentId);
16
+ }
17
+ export async function ensureAskProBrowserProfileDir(agentId, options = {}) {
18
+ const target = askProBrowserProfileDirForAgentId(agentId, options.env);
19
+ const legacy = legacyAskProBrowserProfileDirForAgentId(agentId, options.homeDir);
20
+ const [targetExists, legacyExists] = await Promise.all([exists(target), exists(legacy)]);
21
+ if (targetExists && legacyExists) {
22
+ const recordedIdentity = await retainedLegacyIdentity(target);
23
+ if (recordedIdentity) {
24
+ const currentIdentity = await legacyProfileIdentity(legacy);
25
+ if (currentIdentity && sameLegacyIdentity(currentIdentity, recordedIdentity)) {
26
+ if (await waitForConcurrentMigration(target, legacy))
27
+ return target;
28
+ if (await isBrowserProfileInUse(legacy)) {
29
+ throw new Error("Legacy ask-pro browser profile is in use; retry after its browser run exits.");
30
+ }
31
+ return target;
32
+ }
33
+ throw new Error(`Both current and legacy ask-pro browser profiles exist; refusing to merge ${legacy} into ${target}.`);
34
+ }
35
+ if (await waitForConcurrentMigration(target, legacy))
36
+ return target;
37
+ if ((await exists(target)) && !(await exists(legacy)))
38
+ return target;
39
+ throw new Error(`Both current and legacy ask-pro browser profiles exist; refusing to merge ${legacy} into ${target}.`);
40
+ }
41
+ if (targetExists || !legacyExists)
42
+ return target;
43
+ if (await isBrowserProfileInUse(legacy)) {
44
+ throw new Error("Legacy ask-pro browser profile is in use; retry after its browser run exits.");
45
+ }
46
+ let migrationLock;
47
+ try {
48
+ migrationLock = await acquireProfileRunLock(legacy, {
49
+ timeoutMs: 300_000,
50
+ pollMs: 100,
51
+ requireExistingProfile: true,
52
+ staleLockMode: "fail",
53
+ });
54
+ }
55
+ catch (error) {
56
+ if ((await exists(target)) && !(await exists(legacy)))
57
+ return target;
58
+ throw new Error("Legacy ask-pro browser profile could not be claimed; retry after other browser runs exit.", { cause: error });
59
+ }
60
+ if (!migrationLock)
61
+ throw new Error("Could not claim the legacy ask-pro browser profile.");
62
+ let releasePath = migrationLock.path;
63
+ try {
64
+ if (await matchesRetainedLegacy(target, legacy))
65
+ return target;
66
+ if (await isBrowserProfileInUse(legacy, { ignoreLockId: migrationLock.lockId })) {
67
+ throw new Error("Legacy ask-pro browser profile is in use; retry after its browser run exits.");
68
+ }
69
+ await fs.mkdir(path.dirname(target), { recursive: true });
70
+ let identityRequired = false;
71
+ try {
72
+ await fs.rename(legacy, target);
73
+ releasePath = path.join(target, path.basename(migrationLock.path));
74
+ return target;
75
+ }
76
+ catch (error) {
77
+ const code = error.code;
78
+ if (code !== "EXDEV" && !isSharingDenied(error)) {
79
+ if ((await exists(target)) && !(await exists(legacy)))
80
+ return target;
81
+ throw error;
82
+ }
83
+ identityRequired = code !== "EXDEV";
84
+ }
85
+ const legacyIdentity = await legacyProfileIdentity(legacy);
86
+ if (identityRequired && !legacyIdentity) {
87
+ throw new Error("Legacy ask-pro browser profile identity is unavailable; refusing to copy.");
88
+ }
89
+ const staging = `${target}.migrating-${process.pid}-${randomUUID()}`;
90
+ try {
91
+ await fs.cp(legacy, staging, { recursive: true, errorOnExist: true });
92
+ await fs.writeFile(path.join(staging, MIGRATION_MARKER), JSON.stringify({
93
+ pid: process.pid,
94
+ createdAt: Date.now(),
95
+ migrationLockId: migrationLock.lockId,
96
+ ...(legacyIdentity ? { legacyIdentity } : {}),
97
+ }));
98
+ await fs.rename(staging, target);
99
+ releasePath = path.join(target, path.basename(migrationLock.path));
100
+ }
101
+ catch (error) {
102
+ await fs.rm(staging, { recursive: true, force: true }).catch(() => undefined);
103
+ if ((await exists(target)) && !(await exists(legacy)))
104
+ return target;
105
+ if (await matchesRetainedLegacy(target, legacy))
106
+ return target;
107
+ if (await waitForConcurrentMigration(target, legacy))
108
+ return target;
109
+ if ((await exists(target)) && !(await exists(legacy)))
110
+ return target;
111
+ throw error;
112
+ }
113
+ try {
114
+ await fs.rm(legacy, { recursive: true });
115
+ }
116
+ catch (error) {
117
+ await releaseProfileRunLock(migrationLock.path, migrationLock.lockId);
118
+ if (isSharingDenied(error) && legacyIdentity)
119
+ return target;
120
+ await fs.rm(path.join(target, MIGRATION_MARKER), { force: true });
121
+ if (isSharingDenied(error)) {
122
+ throw new Error("Legacy ask-pro browser profile could not be removed and has no reliable identity; refusing to retain it.", { cause: error });
123
+ }
124
+ throw error;
125
+ }
126
+ await fs.rm(path.join(target, MIGRATION_MARKER), { force: true });
127
+ return target;
128
+ }
129
+ finally {
130
+ await releaseProfileRunLock(releasePath, migrationLock.lockId);
131
+ }
132
+ }
133
+ export function askProAgentIdForLegacyBrowserProfileDir(profileDir) {
134
+ return agentIdForProfileDir(profileDir, path.join(os.homedir(), ".agents", "skills", "ask-pro"));
135
+ }
136
+ export function isLegacyAskProManagedBrowserProfileDir(profileDir) {
137
+ const resolved = normalizeProfilePath(profileDir);
138
+ if (resolved === normalizeProfilePath(legacyAskProBrowserProfileDirForAgentId(null)))
139
+ return true;
140
+ return askProAgentIdForLegacyBrowserProfileDir(profileDir) !== null;
141
+ }
11
142
  function profileDirUnder(root, agentId) {
12
143
  if (!agentId)
13
144
  return path.join(root, "browser-profile");
@@ -45,8 +176,87 @@ function agentIdForProfileDir(profileDir, stateRoot) {
45
176
  return RESOLVED_AGENT_ID_PATTERN.test(agentId) ? agentId : null;
46
177
  }
47
178
  function askProStateDir(env = process.env) {
48
- const override = env.ASK_PRO_STATE_DIR?.trim();
49
- return override ? path.resolve(override) : path.join(os.homedir(), ".agents", "state", "ask-pro");
179
+ const codexHome = env.CODEX_HOME?.trim();
180
+ return path.join(codexHome ? path.resolve(codexHome) : path.join(os.homedir(), ".codex"), "state", "ask-pro");
181
+ }
182
+ async function exists(filePath) {
183
+ return fs
184
+ .stat(filePath)
185
+ .then(() => true)
186
+ .catch((error) => {
187
+ if (error.code === "ENOENT")
188
+ return false;
189
+ throw error;
190
+ });
191
+ }
192
+ async function legacyProfileIdentity(profileDir) {
193
+ const info = await fs.stat(profileDir, { bigint: true }).catch(() => null);
194
+ if (!info?.isDirectory() || info.dev <= 0n || info.ino <= 0n || info.birthtimeNs <= 0n) {
195
+ return null;
196
+ }
197
+ return {
198
+ dev: info.dev.toString(),
199
+ ino: info.ino.toString(),
200
+ birthtimeNs: info.birthtimeNs.toString(),
201
+ };
202
+ }
203
+ async function retainedLegacyIdentity(target) {
204
+ const recorded = await fs
205
+ .readFile(path.join(target, MIGRATION_MARKER), "utf8")
206
+ .then((raw) => JSON.parse(raw).legacyIdentity)
207
+ .catch(() => null);
208
+ if (!recorded ||
209
+ ![recorded.dev, recorded.ino, recorded.birthtimeNs].every((value) => /^[1-9]\d*$/.test(value))) {
210
+ return null;
211
+ }
212
+ return recorded;
213
+ }
214
+ async function matchesRetainedLegacy(target, legacy) {
215
+ const [recorded, current] = await Promise.all([
216
+ retainedLegacyIdentity(target),
217
+ legacyProfileIdentity(legacy),
218
+ ]);
219
+ return recorded !== null && current !== null && sameLegacyIdentity(recorded, current);
220
+ }
221
+ function sameLegacyIdentity(left, right) {
222
+ return left.dev === right.dev && left.ino === right.ino && left.birthtimeNs === right.birthtimeNs;
223
+ }
224
+ function isSharingDenied(error) {
225
+ return ["EACCES", "EBUSY", "EPERM"].includes(error.code ?? "");
226
+ }
227
+ async function waitForConcurrentMigration(target, legacy) {
228
+ const marker = path.join(target, MIGRATION_MARKER);
229
+ const owner = await fs
230
+ .readFile(marker, "utf8")
231
+ .then((raw) => JSON.parse(raw))
232
+ .catch(() => null);
233
+ if (!owner?.pid ||
234
+ !Number.isFinite(owner.pid) ||
235
+ !owner.createdAt ||
236
+ !Number.isFinite(owner.createdAt) ||
237
+ !owner.migrationLockId) {
238
+ return false;
239
+ }
240
+ while (isProcessAlive(owner.pid) &&
241
+ Date.now() - owner.createdAt < 300_000 &&
242
+ (await legacyHasMigrationLock(legacy, owner.migrationLockId))) {
243
+ if (!(await exists(legacy))) {
244
+ await fs.rm(marker, { force: true }).catch(() => undefined);
245
+ return true;
246
+ }
247
+ await new Promise((resolve) => setTimeout(resolve, 250));
248
+ }
249
+ if (await exists(legacy))
250
+ return false;
251
+ await fs.rm(marker, { force: true }).catch(() => undefined);
252
+ return exists(target);
253
+ }
254
+ async function legacyHasMigrationLock(legacy, lockId) {
255
+ const activeLockId = await fs
256
+ .readFile(path.join(legacy, "ask-pro-automation.lock"), "utf8")
257
+ .then((raw) => JSON.parse(raw).lockId)
258
+ .catch(() => null);
259
+ return activeLockId === lockId;
50
260
  }
51
261
  export function resolveAskProAgentId(env = process.env) {
52
262
  const value = env.ASK_PRO_AGENT_ID;
@@ -168,6 +168,42 @@ function parseProfileRunLock(payload) {
168
168
  return null;
169
169
  }
170
170
  }
171
+ export async function isBrowserProfileInUse(userDataDir, options = {}) {
172
+ const lockPath = path.join(userDataDir, ASK_PRO_PROFILE_LOCK_FILENAME);
173
+ let lockPayload;
174
+ try {
175
+ lockPayload = await readFile(lockPath, "utf8");
176
+ }
177
+ catch (error) {
178
+ if (error.code !== "ENOENT")
179
+ return true;
180
+ lockPayload = null;
181
+ }
182
+ let lock = parseProfileRunLock(lockPayload);
183
+ if (lockPayload !== null && !lock) {
184
+ await delay(200);
185
+ try {
186
+ lockPayload = await readFile(lockPath, "utf8");
187
+ }
188
+ catch (error) {
189
+ if (error.code !== "ENOENT")
190
+ return true;
191
+ lockPayload = null;
192
+ }
193
+ lock = parseProfileRunLock(lockPayload);
194
+ }
195
+ if (lock && lock.lockId !== options.ignoreLockId && isProcessAlive(lock.pid))
196
+ return true;
197
+ const pid = await readChromePid(userDataDir);
198
+ const port = await readDevToolsPort(userDataDir);
199
+ const [owner, devTools] = await Promise.all([
200
+ isChromeUsingUserDataDir(userDataDir),
201
+ port ? verifyDevToolsReachable({ port, attempts: 1 }) : null,
202
+ ]);
203
+ if (owner === true || (pid && isProcessAlive(pid) && devTools?.ok))
204
+ return true;
205
+ return owner === null;
206
+ }
171
207
  export async function acquireProfileRunLock(userDataDir, options) {
172
208
  const timeoutMs = options.timeoutMs;
173
209
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
@@ -188,7 +224,12 @@ export async function acquireProfileRunLock(userDataDir, options) {
188
224
  createdAt: new Date().toISOString(),
189
225
  sessionId: options.sessionId,
190
226
  };
191
- await mkdir(path.dirname(lockPath), { recursive: true });
227
+ if (options.requireExistingProfile) {
228
+ await stat(userDataDir);
229
+ }
230
+ else {
231
+ await mkdir(path.dirname(lockPath), { recursive: true });
232
+ }
192
233
  await writeFile(lockPath, JSON.stringify(payload), { encoding: "utf8", flag: "wx" });
193
234
  options.logger?.(`Acquired ask-pro profile lock at ${lockPath}`);
194
235
  return {
@@ -208,12 +249,18 @@ export async function acquireProfileRunLock(userDataDir, options) {
208
249
  await delay(200);
209
250
  existing = parseProfileRunLock(await readFile(lockPath, "utf8").catch(() => null));
210
251
  if (!existing) {
252
+ if (options.staleLockMode === "fail") {
253
+ throw new Error("ask-pro profile lock is unreadable; refusing stale lock removal.");
254
+ }
211
255
  options.logger?.("ask-pro profile lock unreadable; deleting lockfile.");
212
256
  await rm(lockPath, { force: true }).catch(() => undefined);
213
257
  continue;
214
258
  }
215
259
  }
216
260
  if (!existing || !isProcessAlive(existing.pid)) {
261
+ if (options.staleLockMode === "fail") {
262
+ throw new Error("ask-pro profile lock owner is not alive; refusing stale lock removal.");
263
+ }
217
264
  await rm(lockPath, { force: true }).catch(() => undefined);
218
265
  continue;
219
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ask-pro",
3
- "version": "0.1.69",
3
+ "version": "0.1.71",
4
4
  "description": "Browser-backed ChatGPT Pro escalation for hard engineering questions.",
5
5
  "keywords": [
6
6
  "agents",
@@ -51,9 +51,10 @@
51
51
  "dist/src/browser/providers/chatgptDomProvider.js",
52
52
  "dist/src/browserMode.js",
53
53
  "dist/src/version.js",
54
+ "scripts/refresh-local-plugin.mjs",
55
+ "scripts/refresh-local-plugin.ps1",
54
56
  ".codex-plugin/plugin.json",
55
57
  "assets/ask-pro_logo.png",
56
- "SKILL.md",
57
58
  "skills/ask-pro/SKILL.md",
58
59
  "README.md",
59
60
  "LICENSE"
@@ -71,6 +72,7 @@
71
72
  "format:diff": "oxfmt --write . && git --no-pager diff",
72
73
  "lint": "pnpm run typecheck && oxlint",
73
74
  "lint:fix": "oxlint --fix && pnpm run format",
75
+ "plugin:refresh": "pnpm run build && node scripts/refresh-local-plugin.mjs",
74
76
  "test": "pnpm run test:ask-pro",
75
77
  "test:ask-pro": "vitest run tests/ask-pro tests/browser",
76
78
  "test:coverage": "vitest run --coverage",
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const args = parseArgs(process.argv.slice(2));
8
+ const pluginName = args.pluginName ?? "ask-pro";
9
+ const codexHome = args.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
10
+ const marketplacePath = args.marketplacePath ?? (await findCodexMarketplace(codexHome, pluginName));
11
+
12
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
13
+ const repoRoot = path.resolve(scriptDir, "..");
14
+ const marketplaceFile = path.resolve(marketplacePath);
15
+ const marketplace = JSON.parse(await fs.readFile(marketplaceFile, "utf8"));
16
+ const marketplaceName = String(marketplace.name ?? "").trim();
17
+ if (!marketplaceName) {
18
+ throw new Error("Marketplace file must include a top-level name.");
19
+ }
20
+
21
+ const plugin = Array.isArray(marketplace.plugins)
22
+ ? marketplace.plugins.find((entry) => entry?.name === pluginName)
23
+ : null;
24
+ if (!plugin) {
25
+ throw new Error(`Plugin '${pluginName}' was not found in ${marketplaceFile}.`);
26
+ }
27
+ if (plugin.source?.source === "local") {
28
+ const configuredSourcePath = plugin.source?.path;
29
+ if (typeof configuredSourcePath !== "string" || !configuredSourcePath.trim()) {
30
+ throw new Error(`Plugin '${pluginName}' local source must include a path.`);
31
+ }
32
+ const configuredSourceRoot = resolveConfiguredSourceRoot(configuredSourcePath, marketplaceFile);
33
+ if (!samePath(configuredSourceRoot, repoRoot)) {
34
+ throw new Error(
35
+ `Plugin '${pluginName}' marketplace path resolves to ${configuredSourceRoot}, not this checkout ${repoRoot}.`,
36
+ );
37
+ }
38
+ } else if (plugin.source?.source !== "url") {
39
+ throw new Error(`Plugin '${pluginName}' must come from a local or URL marketplace source.`);
40
+ }
41
+
42
+ const manifestPath = path.join(repoRoot, ".codex-plugin", "plugin.json");
43
+ const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
44
+ if (manifest.name !== pluginName) {
45
+ throw new Error(
46
+ `Plugin manifest name '${manifest.name}' does not match requested plugin '${pluginName}'.`,
47
+ );
48
+ }
49
+
50
+ const cacheRoot = path.resolve(codexHome, "plugins", "cache");
51
+ const pluginCacheRoot = path.resolve(cacheRoot, marketplaceName, pluginName);
52
+ const cacheVersion = plugin.source?.source === "local" ? "local" : manifest.version;
53
+ if (typeof cacheVersion !== "string" || !cacheVersion.trim()) {
54
+ throw new Error(`Plugin '${pluginName}' manifest must include a version.`);
55
+ }
56
+ const targetRoot = path.resolve(pluginCacheRoot, cacheVersion);
57
+ assertInside(
58
+ pluginCacheRoot,
59
+ cacheRoot,
60
+ "Resolved plugin cache path is outside Codex plugin cache",
61
+ );
62
+
63
+ const requiredDistEntry = path.join(repoRoot, "dist", "bin", "ask-pro-cli.js");
64
+ if (!(await exists(requiredDistEntry))) {
65
+ throw new Error("dist is missing. Run `pnpm run build` before refreshing the plugin cache.");
66
+ }
67
+ const repoNodeModules = path.join(repoRoot, "node_modules");
68
+ if (!(await exists(repoNodeModules))) {
69
+ throw new Error(
70
+ "node_modules is missing. Run `pnpm install` before refreshing the plugin cache.",
71
+ );
72
+ }
73
+
74
+ await removeNodeModulesLink(path.join(targetRoot, "node_modules"));
75
+ await fs.rm(targetRoot, { recursive: true, force: true });
76
+ await fs.mkdir(targetRoot, { recursive: true });
77
+
78
+ for (const item of [
79
+ ".codex-plugin",
80
+ "assets",
81
+ "skills",
82
+ "references",
83
+ "README.md",
84
+ "LICENSE",
85
+ "package.json",
86
+ "pnpm-lock.yaml",
87
+ "pnpm-workspace.yaml",
88
+ "dist",
89
+ ]) {
90
+ const source = path.join(repoRoot, item);
91
+ if (!(await exists(source))) continue;
92
+ await fs.cp(source, path.join(targetRoot, item), { recursive: true, force: true });
93
+ }
94
+
95
+ await fs.mkdir(path.join(targetRoot, "scripts"), { recursive: true });
96
+ await fs.cp(
97
+ path.join(repoRoot, "scripts", "run-cached-cli.mjs"),
98
+ path.join(targetRoot, "scripts", "run-cached-cli.mjs"),
99
+ { force: true },
100
+ );
101
+ await fs.symlink(repoNodeModules, path.join(targetRoot, "node_modules"), "junction");
102
+
103
+ console.log("Refreshed local Codex plugin cache:");
104
+ console.log(` source: ${repoRoot}`);
105
+ console.log(` target: ${targetRoot}`);
106
+ console.log("");
107
+ console.log("Restart or reload Codex to pick up refreshed plugin skills.");
108
+
109
+ function parseArgs(argv) {
110
+ const result = {};
111
+ for (let i = 0; i < argv.length; i += 1) {
112
+ const arg = argv[i];
113
+ const value = argv[i + 1];
114
+ if (!arg.startsWith("--") || !value || value.startsWith("--")) {
115
+ throw new Error(`Expected --name value argument, got '${arg}'.`);
116
+ }
117
+ i += 1;
118
+ if (arg === "--marketplace-path") result.marketplacePath = value;
119
+ else if (arg === "--codex-home") result.codexHome = value;
120
+ else if (arg === "--plugin-name") result.pluginName = value;
121
+ else throw new Error(`Unknown argument '${arg}'.`);
122
+ }
123
+ return result;
124
+ }
125
+
126
+ function assertInside(child, parent, message) {
127
+ const relative = path.relative(parent, child);
128
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
129
+ throw new Error(`${message}: ${child}`);
130
+ }
131
+ }
132
+
133
+ async function exists(filePath) {
134
+ try {
135
+ await fs.access(filePath);
136
+ return true;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ async function removeNodeModulesLink(filePath) {
143
+ try {
144
+ const stat = await fs.lstat(filePath);
145
+ if (stat.isSymbolicLink()) {
146
+ await fs.unlink(filePath);
147
+ }
148
+ } catch {
149
+ // Missing or non-removable links are handled by the full target cleanup.
150
+ }
151
+ }
152
+
153
+ async function findCodexMarketplace(codexHome, pluginName) {
154
+ const codexMarketplace = path.join(
155
+ codexHome,
156
+ ".tmp",
157
+ "marketplaces",
158
+ pluginName,
159
+ ".agents",
160
+ "plugins",
161
+ "marketplace.json",
162
+ );
163
+ if (await exists(codexMarketplace)) return codexMarketplace;
164
+ return path.join(os.homedir(), ".agents", "plugins", "marketplace.json");
165
+ }
166
+
167
+ function resolveConfiguredSourceRoot(sourcePath, marketplaceFile) {
168
+ const candidates = [
169
+ path.resolve(path.dirname(marketplaceFile), sourcePath),
170
+ path.resolve(os.homedir(), sourcePath),
171
+ ];
172
+ return candidates.find((candidate) => samePath(candidate, repoRoot)) ?? candidates[0];
173
+ }
174
+
175
+ function samePath(left, right) {
176
+ const a = path.resolve(left);
177
+ const b = path.resolve(right);
178
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
179
+ }
@@ -0,0 +1,93 @@
1
+ param(
2
+ [string]$MarketplacePath = "",
3
+ [string]$CodexHome = $(if ($env:CODEX_HOME) { $env:CODEX_HOME } else { "$HOME/.codex" }),
4
+ [string]$PluginName = "ask-pro"
5
+ )
6
+
7
+ $ErrorActionPreference = "Stop"
8
+
9
+ function Resolve-StrictPath([string]$Path) {
10
+ return [System.IO.Path]::GetFullPath((Resolve-Path -LiteralPath $Path).Path)
11
+ }
12
+
13
+ function Join-And-Normalize([string]$Base, [string[]]$Parts) {
14
+ return [System.IO.Path]::GetFullPath((Join-Path -Path $Base -ChildPath ([System.IO.Path]::Combine($Parts))))
15
+ }
16
+
17
+ $repoRoot = Resolve-StrictPath (Join-Path $PSScriptRoot "..")
18
+ if ([string]::IsNullOrWhiteSpace($MarketplacePath)) {
19
+ $codexMarketplace = Join-And-Normalize $CodexHome @(".tmp", "marketplaces", $PluginName, ".agents", "plugins", "marketplace.json")
20
+ if (Test-Path -LiteralPath $codexMarketplace) {
21
+ $MarketplacePath = $codexMarketplace
22
+ } else {
23
+ $MarketplacePath = "$HOME/.agents/plugins/marketplace.json"
24
+ }
25
+ }
26
+ $marketplaceFile = Resolve-StrictPath $MarketplacePath
27
+ $marketplace = Get-Content -LiteralPath $marketplaceFile -Raw | ConvertFrom-Json
28
+ $marketplaceName = [string]$marketplace.name
29
+ if ([string]::IsNullOrWhiteSpace($marketplaceName)) {
30
+ throw "Marketplace file must include a top-level name."
31
+ }
32
+
33
+ $plugin = @($marketplace.plugins) | Where-Object { $_.name -eq $PluginName } | Select-Object -First 1
34
+ if (-not $plugin) {
35
+ throw "Plugin '$PluginName' was not found in $marketplaceFile."
36
+ }
37
+ if (($plugin.source.source -ne "local") -and ($plugin.source.source -ne "url")) {
38
+ throw "Plugin '$PluginName' must come from a local or URL marketplace source."
39
+ }
40
+
41
+ $sourceRoot = Resolve-StrictPath $repoRoot
42
+ $manifestPath = Join-Path $sourceRoot ".codex-plugin/plugin.json"
43
+ if (-not (Test-Path -LiteralPath $manifestPath)) {
44
+ throw "Missing plugin manifest at $manifestPath."
45
+ }
46
+
47
+ $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
48
+ if ($manifest.name -ne $PluginName) {
49
+ throw "Plugin manifest name '$($manifest.name)' does not match requested plugin '$PluginName'."
50
+ }
51
+
52
+ $codexHomePath = [System.IO.Path]::GetFullPath($CodexHome)
53
+ $cacheRoot = Join-And-Normalize $codexHomePath @("plugins", "cache")
54
+ $pluginCacheRoot = Join-And-Normalize $cacheRoot @($marketplaceName, $PluginName)
55
+ $cacheVersion = $(if ($plugin.source.source -eq "local") { "local" } else { [string]$manifest.version })
56
+ if ([string]::IsNullOrWhiteSpace($cacheVersion)) {
57
+ throw "Plugin '$PluginName' manifest must include a version."
58
+ }
59
+ $targetRoot = Join-And-Normalize $pluginCacheRoot @($cacheVersion)
60
+
61
+ $cacheRootWithSeparator = $cacheRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar
62
+ if (-not $pluginCacheRoot.StartsWith($cacheRootWithSeparator, [System.StringComparison]::OrdinalIgnoreCase)) {
63
+ throw "Resolved plugin cache path is outside Codex plugin cache: $pluginCacheRoot"
64
+ }
65
+
66
+ if (Test-Path -LiteralPath $targetRoot) {
67
+ Remove-Item -LiteralPath $targetRoot -Recurse -Force
68
+ }
69
+ New-Item -ItemType Directory -Path $targetRoot -Force | Out-Null
70
+
71
+ $itemsToCopy = @(
72
+ ".codex-plugin",
73
+ "skills",
74
+ "references",
75
+ "README.md",
76
+ "LICENSE",
77
+ "package.json",
78
+ "dist"
79
+ )
80
+
81
+ foreach ($item in $itemsToCopy) {
82
+ $source = Join-Path $sourceRoot $item
83
+ if (-not (Test-Path -LiteralPath $source)) {
84
+ continue
85
+ }
86
+ Copy-Item -LiteralPath $source -Destination $targetRoot -Recurse -Force
87
+ }
88
+
89
+ Write-Host "Refreshed local Codex plugin cache:"
90
+ Write-Host " source: $sourceRoot"
91
+ Write-Host " target: $targetRoot"
92
+ Write-Host ""
93
+ Write-Host "Restart or reload Codex to pick up refreshed plugin skills."
@@ -5,7 +5,232 @@ description: Escalate hard engineering questions to ChatGPT Pro through browser
5
5
 
6
6
  # $ask-pro
7
7
 
8
- This is the Codex plugin entry for ask-pro. The full instructions are in
9
- `SKILL.md` at the plugin root, two folders above this file. Read that file and
10
- follow it. Its `<skill-folder>` is the plugin root, so the launcher is
11
- `node <plugin-root>/scripts/ask-pro.mjs`.
8
+ Use `$ask-pro` to ask ChatGPT Pro for a focused second opinion on hard engineering work.
9
+
10
+ The calling agent still owns the work. Use Pro for judgment, architecture, risk review, or implementation planning when the decision is consequential enough to justify a browser run.
11
+
12
+ ## Trigger
13
+
14
+ Use this skill when the user explicitly asks for `$ask-pro`, or when a second opinion would materially reduce risk for:
15
+
16
+ - backend architecture
17
+ - schema or data migrations
18
+ - auth, sessions, permissions, or billing
19
+ - queues, workers, idempotency, caching, scaling, or latency
20
+ - production debugging and observability
21
+ - ambiguous implementation paths where a second opinion would reduce risk
22
+
23
+ Do not use it for trivial syntax fixes, formatting, obvious dependency updates, or small bugs with a clear cause.
24
+
25
+ ## Critical Reminders
26
+
27
+ - Long waits are normal. ask-pro can take a super long time to run; do not
28
+ close or kill the browser window/run until at least 3 hours have passed unless
29
+ the CLI has completed, failed, or explicitly asks for human action.
30
+
31
+ ## Workflow
32
+
33
+ When invoked:
34
+
35
+ 1. Inspect the repo and the relevant files.
36
+ 2. Identify the exact decision Pro should answer.
37
+ 3. For committed code on GitHub, prefer `--github` and give Pro the repository,
38
+ ref, relevant paths, and question. Let it retrieve the code instead of assembling
39
+ a source bundle. Add a small `--files` bundle only for uncommitted changes,
40
+ local-only evidence, or repository content the connection cannot access.
41
+ 4. Write the prompt yourself using the Prompt Shape below.
42
+ 5. Run the smallest useful command: `ask-pro --github "<prompt>"` for GitHub
43
+ evidence, or `ask-pro --no-temporary --files "<glob>" "<prompt>"` for a file bundle.
44
+ For multiline prompts, write a temporary prompt file and use
45
+ `ask-pro --no-temporary --prompt-file <path> --files "<glob>"`; do not rely
46
+ on shell multiline quoting.
47
+ If installed through the skills CLI and `ask-pro` is not on `PATH`, install
48
+ the standalone CLI from the [README](https://github.com/JJLiebig/ask-pro#skills-cli-codex-and-other-supported-agents)
49
+ first. For Codex plugin installs without `ask-pro` on `PATH`, use the cached
50
+ plugin runner. Locate it under the installed plugin cache,
51
+ usually
52
+ `~/.codex/plugins/cache/<marketplace-name>/ask-pro/<version>/scripts/run-cached-cli.mjs`,
53
+ then call it with:
54
+ `node <cached-runner> -- --cwd <target-repo-root> --no-temporary --prompt-file <path> --files "<repo-relative-glob>"`.
55
+ On Git marketplace installs, the first cached-runner call may bootstrap the
56
+ content-addressed runtime under `$CODEX_HOME/plugin-runtimes/ask-pro/` by
57
+ installing dependencies and building `dist`; wait for that to finish. The
58
+ installed plugin cache stays immutable.
59
+ 6. If auth is required, stop and ask the human to log in in the opened browser.
60
+ 7. Read the CLI's compact `ask_pro` record and run the emitted `resume` or
61
+ `harvest` command when that is the next action.
62
+ 8. Treat the answer as advisory; turn it into your own plan before editing code.
63
+
64
+ `ask-pro` selects `Latest` when available, otherwise the highest numbered available
65
+ GPT model, then `Pro` intelligence. Do not require a
66
+ different dated model label or a separate thinking-effort control in the prompt
67
+ or workflow.
68
+
69
+ Fresh runs try ChatGPT Temporary Chat by default and automatically fall back to
70
+ normal ChatGPT if the current account/UI does not expose Pro there. For repo
71
+ advisories, large bundles, review rounds, or anything where recovery matters,
72
+ prefer `--no-temporary` from the start. Add `--temporary` only when Temporary
73
+ Chat is required and falling back would be wrong. Temporary Chat is less
74
+ recoverable after browser/tab loss.
75
+
76
+ On Windows, ordinary managed Chrome runs start minimized. First login,
77
+ resume/recovery, and stale-auth paths stay visible or are restored for human
78
+ action. Local managed Chrome guards browser input while Pro is answering.
79
+ If login, MFA, a browser challenge, or incomplete-answer debugging needs human
80
+ attention, ask-pro should restore or retain the browser and emit the next
81
+ action.
82
+
83
+ Do not set `ASK_PRO_AGENT_ID` for ordinary or concurrent use; the shared
84
+ `ask-pro` browser profile under `$CODEX_HOME/state/ask-pro/` handles concurrent
85
+ agents automatically. Set `ASK_PRO_AGENT_ID` only when explicitly testing an
86
+ isolated profile or when the human requests a separate browser login. Use a
87
+ stable reusable lowercase id like `review-t1`, not a one-off task slug, because
88
+ each new id creates a new Chrome profile and may require the human to log in
89
+ again. Example:
90
+ `ASK_PRO_AGENT_ID=review-t1 ask-pro ...`.
91
+
92
+ ## Prompt Shape
93
+
94
+ Assume Pro has no caller context or local checkout. Include each material fact and
95
+ instruction once: the goal, current state, hard constraints, evidence, success
96
+ criteria, and required output. Do not rely on the agent's conversation context, repo
97
+ folklore, prior ask-pro runs, branch names, or unstated user preferences.
98
+ Keep advisory design consults as plain answer requests. Start advisory prompts
99
+ with:
100
+
101
+ ```text
102
+ Return final Markdown only, with no preamble or implementation package.
103
+ ```
104
+
105
+ The CLI wrapper already tells Pro to read `CONTEXT.zip` and call out missing or
106
+ conflicting context. Do not repeat that instruction. Add task-specific output
107
+ requirements only when needed. For risk or review work, ask Pro to rank
108
+ findings by severity. For architecture or design consults, ask directly for the
109
+ recommendation and tradeoffs.
110
+
111
+ Use `--artifacts` only when the standard implementation package is needed. The
112
+ wrapper supplies the zip name, standard files, and markdown fallback; add only
113
+ task-specific deliverables not covered by that package. Keep advisory consults
114
+ inline by default.
115
+ Keep bundles focused: source files under review, focused tests, relevant docs,
116
+ known recent changes, and validation status. Avoid whole-repo bundles unless the
117
+ question is explicitly architectural.
118
+
119
+ ## Connected GitHub (optional)
120
+
121
+ Prefer `--github` for remote repository evidence. It checks
122
+ the connection before uploading or submitting, and defaults to normal ChatGPT.
123
+ Do not require GitHub for file-based consults. Availability varies by account,
124
+ workspace, model, and ChatGPT experience.
125
+
126
+ If the CLI returns `needs_github_connection` / `connect_github_then_resume`,
127
+ direct the human to the opened GitHub page in ask-pro's browser. They install the
128
+ plugin if needed, complete sign-in, review permissions, and select repositories;
129
+ organization approval may be needed. Wait for them to finish, then run the
130
+ emitted resume command without adding `--github`. The requirement is saved with
131
+ the session and checked again. Do not start a second session or submit through
132
+ the browser yourself. Never collect GitHub credentials or automate authorization.
133
+
134
+ If available, GitHub's **Allow read actions** permission can avoid read approval
135
+ prompts. The human chooses that setting. It controls approval behavior, not the
136
+ underlying permissions of a write-capable app; see
137
+ [app permissions](https://help.openai.com/en/articles/20001495-managing-app-permissions-in-chatgpt).
138
+ The live GitHub plugin advertises write capabilities despite the Help Center's
139
+ read-only description. `--github` requests read-only work; it does not restrict
140
+ the provider's permissions or prove that writes are impossible.
141
+
142
+ If GitHub is already connected, proceed without a setup question or source-file
143
+ bundle. For the consult, name the repository (`owner/repo`), relevant paths, and
144
+ requested branch or commit in the prompt. Explicitly ask Pro
145
+ to answer the specific question. The wrapper requests the connected GitHub app,
146
+ read-only work, citations, the revision inspected, and access gaps. Do not
147
+ infer repository access from ChatGPT login or a successful CLI exit.
148
+
149
+ Keep `--files` for local/uncommitted changes, non-GitHub evidence, and snapshots
150
+ that the app cannot retrieve. Distinguish
151
+ uploaded evidence from GitHub evidence, and ask Pro to flag revision conflicts.
152
+ If the app or requested revision is unavailable, offer a new consult with a
153
+ focused file bundle and
154
+ disclose that limitation. Do not silently switch away from Pro. The CLI submits
155
+ text; it does not select an app in the composer or handle app approval prompts.
156
+ Until a live run demonstrates retrieval, describe this as an available setup
157
+ path, not a verified connection. Confirm material claims against the repo.
158
+
159
+ ## Output
160
+
161
+ Normal `ask-pro` stdout is compact TOON-style telemetry. Use `state`, `action`,
162
+ `resume`, and `harvest` to decide the next command. Browser progress may appear
163
+ on stderr and can be ignored unless diagnosing a stuck run.
164
+
165
+ `AWAITING_ANSWERS` / `answer_questions` means Pro is asking native interactive
166
+ questions. Read the printed questions/options or `QUESTIONS.json`, answer from
167
+ the caller's context, and ask the human only for decisions you cannot infer.
168
+ Write UTF-8 JSON `{"requestId":"<request_id>","answers":[2,3,"free text"]}`
169
+ with one answer per question in order, then run the emitted
170
+ `ask-pro --resume <session-id> --answers <answers.json>` command. Numbers select
171
+ 1-based choices; strings enter the native free-text field. `--answers -` accepts
172
+ piped JSON. Do not submit defaults or treat the question preamble as the answer.
173
+ Repeat for further question rounds, then harvest the final answer normally.
174
+
175
+ When present, use `profile`, `profile_path`, `chrome`, and `language` only as
176
+ diagnostic hints. They tell you whether the run used the shared profile, an
177
+ isolated agent profile, saved DevTools state, and English browser steering.
178
+ When present, `conversation_url` is a recoverable non-temporary ChatGPT
179
+ conversation URL.
180
+
181
+ `ask-pro --harvest <session-id>` prints the raw markdown answer only for
182
+ answer-bearing states such as `COMPLETED` or `HARVESTED`.
183
+ For pending/incomplete sessions it prints compact status/action instead. For
184
+ sessions run with `--artifacts`, any provided `ask-pro-response.zip` is
185
+ extracted under the session's `pro-output/` directory and described in
186
+ `PRO_OUTPUT_MANIFEST.json`. Inline-default sessions should not expect a zip.
187
+
188
+ `INCOMPLETE_ANSWER` / `stopped_without_answer` means ChatGPT stopped without
189
+ an answer after ask-pro attempted one automatic `continue`. Present the caller
190
+ with the emitted resume command or a full retry of the original request in a new
191
+ chat, and wait for their choice. Do not automatically resume or retry this state.
192
+ An explicit `--resume` allows one more continuation attempt.
193
+
194
+ `COMPLETED` means harvest now; the run browser may already be closed. If the
195
+ state is `INCOMPLETE_ANSWER` / `preamble_without_artifacts`, do not treat
196
+ `ANSWER.md` as final. Try resume/harvest if recoverable; otherwise rerun with
197
+ `--no-temporary`, a tighter bundle, and a more direct prompt.
198
+
199
+ ## Commands
200
+
201
+ ```bash
202
+ ask-pro "Review the async billing webhook migration plan and return an implementation plan."
203
+ ask-pro --no-temporary --prompt-file question.md --files src --files tests
204
+ ask-pro --github "In owner/repo at <commit>, inspect src/auth and explain token validation."
205
+ ask-pro --temporary "Review this sensitive migration plan, and fail if Temporary Chat cannot use Pro."
206
+ ask-pro --no-temporary "Review this in normal ChatGPT instead of Temporary Chat."
207
+ ask-pro --prompt-file question.md --files .\src
208
+ ask-pro --artifacts --prompt-file implementation-plan.md --files src
209
+ ask-pro --files "src/api/stripe/**" --files "prisma/**" --files "src/lib/billing/**" \
210
+ "Review whether this Stripe webhook flow should use a queue or transactional outbox."
211
+ ask-pro --dry-run "Prepare the Pro handoff but do not open the browser."
212
+ ask-pro --resume <session-id>
213
+ ask-pro --harvest <session-id>
214
+ ```
215
+
216
+ If the binary is not on `PATH`, skills CLI installs need the standalone CLI from
217
+ the README. Codex plugin installs can use the cached plugin runner. Do not run
218
+ the plugin from a mutable development checkout; it may contain in-flight changes
219
+ that have not been synced for agents.
220
+
221
+ ```bash
222
+ node <cached-runner> -- --cwd /path/to/repo --no-temporary --prompt-file question.md --files src
223
+ ```
224
+
225
+ In cached-runner fallback mode, `--files` must be inside `--cwd`. Use
226
+ repo-relative `--files`; do not point at files outside the target repo cwd.
227
+
228
+ ## Safety
229
+
230
+ Never ask for, read, store, type, or log passwords, MFA codes, recovery codes, session cookies, or raw auth tokens.
231
+
232
+ Browser auth is human-controlled. Continue only after the human says the ChatGPT composer is visible.
233
+
234
+ After submit, avoid interacting with any retained Chrome run window while Pro is
235
+ thinking. ask-pro guards input after submit, but the safest agent behavior is to
236
+ let the run finish or resume/harvest from CLI telemetry.
package/SKILL.md DELETED
@@ -1,229 +0,0 @@
1
- ---
2
- name: ask-pro
3
- description: Escalate hard engineering questions to ChatGPT Pro through browser automation with focused repo context. Use when an agent needs a stronger external review, architecture plan, migration strategy, production-debugging second opinion, or when the user explicitly asks to use $ask-pro.
4
- ---
5
-
6
- # $ask-pro
7
-
8
- Use `$ask-pro` to ask ChatGPT Pro for a focused second opinion on hard engineering work.
9
-
10
- The calling agent still owns the work. Use Pro for judgment, architecture, risk review, or implementation planning when the decision is consequential enough to justify a browser run.
11
-
12
- ## Running ask-pro
13
-
14
- This skill folder contains ask-pro itself; nothing else needs installing. In
15
- the commands below, `ask-pro` stands for:
16
-
17
- ```bash
18
- node <skill-folder>/scripts/ask-pro.mjs
19
- ```
20
-
21
- `<skill-folder>` is the directory containing this `SKILL.md`. Run it from the
22
- target repo root. The first run after an install or update installs
23
- dependencies and builds, which can take a few minutes; wait for it. The CLI's
24
- emitted `resume` and `harvest` commands already contain the full launcher path;
25
- run them as printed. Node.js 24+, npm, and Chrome are required.
26
-
27
- ## Trigger
28
-
29
- Use this skill when the user explicitly asks for `$ask-pro`, or when a second opinion would materially reduce risk for:
30
-
31
- - backend architecture
32
- - schema or data migrations
33
- - auth, sessions, permissions, or billing
34
- - queues, workers, idempotency, caching, scaling, or latency
35
- - production debugging and observability
36
- - ambiguous implementation paths where a second opinion would reduce risk
37
-
38
- Do not use it for trivial syntax fixes, formatting, obvious dependency updates, or small bugs with a clear cause.
39
-
40
- ## Critical Reminders
41
-
42
- - Long waits are normal. ask-pro can take a super long time to run; do not
43
- close or kill the browser window/run until at least 3 hours have passed unless
44
- the CLI has completed, failed, or explicitly asks for human action.
45
-
46
- ## Workflow
47
-
48
- When invoked:
49
-
50
- 1. Inspect the repo and the relevant files.
51
- 2. Identify the exact decision Pro should answer.
52
- 3. For committed code on GitHub, prefer `--github` and give Pro the repository,
53
- ref, relevant paths, and question. Let it retrieve the code instead of assembling
54
- a source bundle. Add a small `--files` bundle only for uncommitted changes,
55
- local-only evidence, or repository content the connection cannot access.
56
- 4. Write the prompt yourself using the Prompt Shape below.
57
- 5. Run the smallest useful command: `ask-pro --github "<prompt>"` for GitHub
58
- evidence, or `ask-pro --no-temporary --files "<glob>" "<prompt>"` for a file bundle.
59
- For multiline prompts, write a temporary prompt file and use
60
- `ask-pro --no-temporary --prompt-file <path> --files "<glob>"`; do not rely
61
- on shell multiline quoting.
62
- 6. If auth is required, stop and ask the human to log in in the opened browser.
63
- 7. Read the CLI's compact `ask_pro` record and run the emitted `resume` or
64
- `harvest` command when that is the next action.
65
- 8. Treat the answer as advisory; turn it into your own plan before editing code.
66
-
67
- `ask-pro` selects `Latest` when available, otherwise the highest numbered available
68
- GPT model, then `Pro` intelligence. Do not require a
69
- different dated model label or a separate thinking-effort control in the prompt
70
- or workflow.
71
-
72
- Fresh runs try ChatGPT Temporary Chat by default and automatically fall back to
73
- normal ChatGPT if the current account/UI does not expose Pro there. For repo
74
- advisories, large bundles, review rounds, or anything where recovery matters,
75
- prefer `--no-temporary` from the start. Add `--temporary` only when Temporary
76
- Chat is required and falling back would be wrong. Temporary Chat is less
77
- recoverable after browser/tab loss.
78
-
79
- On Windows, ordinary managed Chrome runs start minimized. First login,
80
- resume/recovery, and stale-auth paths stay visible or are restored for human
81
- action. Local managed Chrome guards browser input while Pro is answering.
82
- If login, MFA, a browser challenge, or incomplete-answer debugging needs human
83
- attention, ask-pro should restore or retain the browser and emit the next
84
- action.
85
-
86
- Do not set `ASK_PRO_AGENT_ID` for ordinary or concurrent use; the shared
87
- `ask-pro` browser profile under `~/.agents/state/ask-pro/` handles concurrent
88
- agents automatically. Set `ASK_PRO_AGENT_ID` only when explicitly testing an
89
- isolated profile or when the human requests a separate browser login. Use a
90
- stable reusable lowercase id like `review-t1`, not a one-off task slug, because
91
- each new id creates a new Chrome profile and may require the human to log in
92
- again. Example:
93
- `ASK_PRO_AGENT_ID=review-t1 ask-pro ...`.
94
-
95
- ## Prompt Shape
96
-
97
- Assume Pro has no caller context or local checkout. Include each material fact and
98
- instruction once: the goal, current state, hard constraints, evidence, success
99
- criteria, and required output. Do not rely on the agent's conversation context, repo
100
- folklore, prior ask-pro runs, branch names, or unstated user preferences.
101
- Keep advisory design consults as plain answer requests. Start advisory prompts
102
- with:
103
-
104
- ```text
105
- Return final Markdown only, with no preamble or implementation package.
106
- ```
107
-
108
- The CLI wrapper already tells Pro to read `CONTEXT.zip` and call out missing or
109
- conflicting context. Do not repeat that instruction. Add task-specific output
110
- requirements only when needed. For risk or review work, ask Pro to rank
111
- findings by severity. For architecture or design consults, ask directly for the
112
- recommendation and tradeoffs.
113
-
114
- Use `--artifacts` only when the standard implementation package is needed. The
115
- wrapper supplies the zip name, standard files, and markdown fallback; add only
116
- task-specific deliverables not covered by that package. Keep advisory consults
117
- inline by default.
118
- Keep bundles focused: source files under review, focused tests, relevant docs,
119
- known recent changes, and validation status. Avoid whole-repo bundles unless the
120
- question is explicitly architectural.
121
-
122
- ## Connected GitHub (optional)
123
-
124
- Prefer `--github` for remote repository evidence. It checks
125
- the connection before uploading or submitting, and defaults to normal ChatGPT.
126
- Do not require GitHub for file-based consults. Availability varies by account,
127
- workspace, model, and ChatGPT experience.
128
-
129
- If the CLI returns `needs_github_connection` / `connect_github_then_resume`,
130
- direct the human to the opened GitHub page in ask-pro's browser. They install the
131
- plugin if needed, complete sign-in, review permissions, and select repositories;
132
- organization approval may be needed. Wait for them to finish, then run the
133
- emitted resume command without adding `--github`. The requirement is saved with
134
- the session and checked again. Do not start a second session or submit through
135
- the browser yourself. Never collect GitHub credentials or automate authorization.
136
-
137
- If available, GitHub's **Allow read actions** permission can avoid read approval
138
- prompts. The human chooses that setting. It controls approval behavior, not the
139
- underlying permissions of a write-capable app; see
140
- [app permissions](https://help.openai.com/en/articles/20001495-managing-app-permissions-in-chatgpt).
141
- The live GitHub plugin advertises write capabilities despite the Help Center's
142
- read-only description. `--github` requests read-only work; it does not restrict
143
- the provider's permissions or prove that writes are impossible.
144
-
145
- If GitHub is already connected, proceed without a setup question or source-file
146
- bundle. For the consult, name the repository (`owner/repo`), relevant paths, and
147
- requested branch or commit in the prompt. Explicitly ask Pro
148
- to answer the specific question. The wrapper requests the connected GitHub app,
149
- read-only work, citations, the revision inspected, and access gaps. Do not
150
- infer repository access from ChatGPT login or a successful CLI exit.
151
-
152
- Keep `--files` for local/uncommitted changes, non-GitHub evidence, and snapshots
153
- that the app cannot retrieve. Distinguish
154
- uploaded evidence from GitHub evidence, and ask Pro to flag revision conflicts.
155
- If the app or requested revision is unavailable, offer a new consult with a
156
- focused file bundle and
157
- disclose that limitation. Do not silently switch away from Pro. The CLI submits
158
- text; it does not select an app in the composer or handle app approval prompts.
159
- Until a live run demonstrates retrieval, describe this as an available setup
160
- path, not a verified connection. Confirm material claims against the repo.
161
-
162
- ## Output
163
-
164
- Normal `ask-pro` stdout is compact TOON-style telemetry. Use `state`, `action`,
165
- `resume`, and `harvest` to decide the next command. Browser progress may appear
166
- on stderr and can be ignored unless diagnosing a stuck run.
167
-
168
- `AWAITING_ANSWERS` / `answer_questions` means Pro is asking native interactive
169
- questions. Read the printed questions/options or `QUESTIONS.json`, answer from
170
- the caller's context, and ask the human only for decisions you cannot infer.
171
- Write UTF-8 JSON `{"requestId":"<request_id>","answers":[2,3,"free text"]}`
172
- with one answer per question in order, then run the emitted
173
- `ask-pro --resume <session-id> --answers <answers.json>` command. Numbers select
174
- 1-based choices; strings enter the native free-text field. `--answers -` accepts
175
- piped JSON. Do not submit defaults or treat the question preamble as the answer.
176
- Repeat for further question rounds, then harvest the final answer normally.
177
-
178
- When present, use `profile`, `profile_path`, `chrome`, and `language` only as
179
- diagnostic hints. They tell you whether the run used the shared profile, an
180
- isolated agent profile, saved DevTools state, and English browser steering.
181
- When present, `conversation_url` is a recoverable non-temporary ChatGPT
182
- conversation URL.
183
-
184
- `ask-pro --harvest <session-id>` prints the raw markdown answer only for
185
- answer-bearing states such as `COMPLETED` or `HARVESTED`.
186
- For pending/incomplete sessions it prints compact status/action instead. For
187
- sessions run with `--artifacts`, any provided `ask-pro-response.zip` is
188
- extracted under the session's `pro-output/` directory and described in
189
- `PRO_OUTPUT_MANIFEST.json`. Inline-default sessions should not expect a zip.
190
-
191
- `INCOMPLETE_ANSWER` / `stopped_without_answer` means ChatGPT stopped without
192
- an answer after ask-pro attempted one automatic `continue`. Present the caller
193
- with the emitted resume command or a full retry of the original request in a new
194
- chat, and wait for their choice. Do not automatically resume or retry this state.
195
- An explicit `--resume` allows one more continuation attempt.
196
-
197
- `COMPLETED` means harvest now; the run browser may already be closed. If the
198
- state is `INCOMPLETE_ANSWER` / `preamble_without_artifacts`, do not treat
199
- `ANSWER.md` as final. Try resume/harvest if recoverable; otherwise rerun with
200
- `--no-temporary`, a tighter bundle, and a more direct prompt.
201
-
202
- ## Commands
203
-
204
- ```bash
205
- ask-pro "Review the async billing webhook migration plan and return an implementation plan."
206
- ask-pro --no-temporary --prompt-file question.md --files src --files tests
207
- ask-pro --github "In owner/repo at <commit>, inspect src/auth and explain token validation."
208
- ask-pro --temporary "Review this sensitive migration plan, and fail if Temporary Chat cannot use Pro."
209
- ask-pro --no-temporary "Review this in normal ChatGPT instead of Temporary Chat."
210
- ask-pro --prompt-file question.md --files .\src
211
- ask-pro --artifacts --prompt-file implementation-plan.md --files src
212
- ask-pro --files "src/api/stripe/**" --files "prisma/**" --files "src/lib/billing/**" \
213
- "Review whether this Stripe webhook flow should use a queue or transactional outbox."
214
- ask-pro --dry-run "Prepare the Pro handoff but do not open the browser."
215
- ask-pro --resume <session-id>
216
- ask-pro --harvest <session-id>
217
- ```
218
-
219
- `--files` must be inside the target repo. Use repo-relative paths.
220
-
221
- ## Safety
222
-
223
- Never ask for, read, store, type, or log passwords, MFA codes, recovery codes, session cookies, or raw auth tokens.
224
-
225
- Browser auth is human-controlled. Continue only after the human says the ChatGPT composer is visible.
226
-
227
- After submit, avoid interacting with any retained Chrome run window while Pro is
228
- thinking. ask-pro guards input after submit, but the safest agent behavior is to
229
- let the run finish or resume/harvest from CLI telemetry.