@youdie006/prodex 0.40.40 → 0.40.42

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
@@ -163,7 +163,7 @@ Power slider on this account (the slider was walked and put back):
163
163
  * 5/5 6 - Pro
164
164
  ```
165
165
 
166
- - `--effort 즉시|중간|높음|"매우 높음"|Pro` picks a rung; English aliases `instant`, `medium`, `high`, `extrahigh` and `max` are accepted, and `--model Pro` reaches the same top rung. Korean and English (US) ChatGPT labels are both matched; on another display language, pass the exact label `models` shows.
166
+ - `--effort 즉시|중간|높음|"매우 높음"|Pro` picks a rung; English aliases `instant`, `medium`, `high`, `extrahigh` and `max` are accepted, and a step name given as a model (`--model Pro`, `--model Instant`) is applied to the slider with a `model_is_now_an_effort` warning. Korean and English (US) ChatGPT labels are both matched; on another display language, pass the exact label `models` shows.
167
167
  - ChatGPT now has two surfaces, Chat and Work, with different pickers; Work's ladder ends in Max and Ultra and offers no Pro. prodex puts the browser back on Chat before a send (and says so on the receipt), so a drifted browser cannot quietly send on the wrong picker. `Max` and `Ultra` are accepted for a browser already on Work.
168
168
  - The model rows themselves (Latest, GPT-5.6 Sol, GPT-5.5) cannot be clicked by automation in the current picker; the slider is the lever, and prodex says so rather than pretending a row was chosen.
169
169
  - Selection is guarded: a control that is covered or off screen is not clicked, a menu that stays open after a pick counts as a failed pick, and any failure backs out with Escape and reports a blocker instead of sending with the wrong model. What was applied is recorded on the receipt (`metadata.selection`, project name redacted).
@@ -410,7 +410,43 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
410
410
  // A sent user turn renders inline code as <code>, dropping its backticks
411
411
  // from innerText (measured 2026-10-07), and the request is matched against
412
412
  // what was typed. Each inline code span gets its backticks back, in page order.
413
+ const inlineCodes = (root) =>
414
+ [...root.querySelectorAll('code')].filter((code) => {
415
+ if (code.closest('pre')) return false;
416
+ const value = code.textContent || "";
417
+ // A fenced block in a user turn is one <code> outside any <pre>
418
+ // (measured); inline code is a single line.
419
+ return value.length > 0 && !value.includes(String.fromCharCode(10));
420
+ });
421
+ // Backtick text nodes go in beside each span for one innerText read and come
422
+ // straight out again, so they land where the span is. Searching the text for
423
+ // the span's value put them on an earlier plain occurrence of the same words
424
+ // and refused a request that had posted (measured 2026-10-24). All of it runs
425
+ // in one synchronous evaluation, so the page never renders in between.
426
+ const userMessageTextAtCodePositions = (root) => {
427
+ const doc = root.ownerDocument;
428
+ if (!doc || typeof doc.createTextNode !== "function") return undefined;
429
+ const tick = String.fromCharCode(96);
430
+ const inserted = [];
431
+ try {
432
+ for (const code of inlineCodes(root)) {
433
+ const parent = code.parentNode;
434
+ if (!parent || typeof parent.insertBefore !== "function") return undefined;
435
+ const before = doc.createTextNode(tick);
436
+ const after = doc.createTextNode(tick);
437
+ parent.insertBefore(before, code);
438
+ inserted.push(before);
439
+ parent.insertBefore(after, code.nextSibling);
440
+ inserted.push(after);
441
+ }
442
+ return root.innerText || "";
443
+ } finally {
444
+ for (const node of inserted) node.parentNode?.removeChild(node);
445
+ }
446
+ };
413
447
  const userMessageText = (root) => {
448
+ const placed = userMessageTextAtCodePositions(root);
449
+ if (placed !== undefined) return placed;
414
450
  let text = root.innerText || "";
415
451
  const tick = String.fromCharCode(96);
416
452
  let cursor = 0;
@@ -1254,23 +1290,48 @@ export function proModeNotAppliedWarning(proMode, sliderEffort) {
1254
1290
  */
1255
1291
  export function pickerSelectionPlan(input) {
1256
1292
  const plan = {};
1257
- const modelIsNowAnEffort = input.model !== undefined && /^\s*pro\s*$/i.test(input.model);
1293
+ const modelAsEffort = input.model !== undefined ? chatStepNamedAsModel(input.model) : undefined;
1258
1294
  if (input.effort !== undefined)
1259
1295
  plan.sliderLabel = input.effort;
1260
- else if (modelIsNowAnEffort)
1261
- plan.sliderLabel = input.model;
1262
- if (input.model !== undefined && !modelIsNowAnEffort)
1296
+ else if (modelAsEffort !== undefined)
1297
+ plan.sliderLabel = modelAsEffort;
1298
+ if (input.model !== undefined && modelAsEffort === undefined)
1263
1299
  plan.modelLabel = input.model;
1264
- if (modelIsNowAnEffort) {
1300
+ if (modelAsEffort !== undefined) {
1265
1301
  plan.warning =
1266
- `model_is_now_an_effort: ChatGPT's picker lists Pro as an effort step, not a model, so --model ${input.model} was ` +
1302
+ `model_is_now_an_effort: ChatGPT's picker lists ${input.model.trim()} as an effort step, not a model, so --model ${input.model} was ` +
1267
1303
  (input.effort === undefined
1268
- ? "applied to the effort slider instead. Say --effort Pro to be explicit"
1304
+ ? `applied to the effort slider instead. Say --effort ${input.model.trim()} to be explicit`
1269
1305
  : `ignored in favour of --effort ${input.effort}`) +
1270
1306
  ", or clear a saved default with `prodex setup --clear-model`.";
1271
1307
  }
1272
1308
  return plan;
1273
1309
  }
1310
+ /**
1311
+ * The Chat slider step a --model value names, if it names one.
1312
+ *
1313
+ * Pro was the first model to become a step, but Instant through Extra High sit
1314
+ * on the same slider: `--model Instant` warned "does not offer" and went out on
1315
+ * whatever step was saved, so a request for a fast answer waited on Pro
1316
+ * reasoning (measured 2026-10-22). Max and Ultra are Work-only rungs that a
1317
+ * model name does not reach, so they stay model names.
1318
+ */
1319
+ export function chatStepNamedAsModel(model) {
1320
+ let step;
1321
+ try {
1322
+ step = parseReasoningEffort(model);
1323
+ }
1324
+ catch {
1325
+ return undefined;
1326
+ }
1327
+ if (step === "Max" || step === "Ultra")
1328
+ return undefined;
1329
+ // "max" is an alias of 매우 높음 for --effort, but a model called Max is
1330
+ // asking for the Work rung, not for Extra High.
1331
+ if (/^\s*max\s*$/i.test(model))
1332
+ return undefined;
1333
+ return step;
1334
+ }
1274
1335
  export function chatGptBusyBlocker(state) {
1275
1336
  // Waiting for a choice is not generating. Conflating them turned a click
1276
1337
  // away from ready into a full send budget spent on "still generating".
@@ -1460,6 +1521,28 @@ export function normalizeChatGptTargetUrl(value) {
1460
1521
  url.pathname = url.pathname.replace(/\/+$/, "") || "/";
1461
1522
  return url.toString();
1462
1523
  }
1524
+ /**
1525
+ * A conversation URL, the only thing recovery can read an answer from.
1526
+ *
1527
+ * Recovery navigates the shared tab to its target. A URL that only passed the
1528
+ * host check (`/c/abc def`, the home page, a project page) took the send lock
1529
+ * and moved the tab somewhere no answer could be (measured 2026-10-26).
1530
+ */
1531
+ export function normalizeChatGptThreadUrl(value) {
1532
+ const describe = `It must be a ChatGPT conversation URL such as https://chatgpt.com/c/<id> (or https://chatgpt.com/g/<project>/c/<id>), not ${JSON.stringify(value)}.`;
1533
+ let url;
1534
+ try {
1535
+ url = normalizeChatGptTargetUrl(value);
1536
+ }
1537
+ catch {
1538
+ throw new Error(`Recovery target is not a ChatGPT URL. ${describe}`);
1539
+ }
1540
+ const pathname = new URL(url).pathname;
1541
+ if (!/^(?:\/g\/[A-Za-z0-9_-]+)?\/c\/[A-Za-z0-9][A-Za-z0-9_-]*$/.test(pathname)) {
1542
+ throw new Error(`Recovery target is not a conversation URL. ${describe}`);
1543
+ }
1544
+ return url;
1545
+ }
1463
1546
  export function chatGptUrlsReferToSameTarget(currentUrl, expectedUrl) {
1464
1547
  try {
1465
1548
  return normalizeChatGptTargetUrl(currentUrl) === normalizeChatGptTargetUrl(expectedUrl);
@@ -3741,7 +3824,7 @@ export async function recoverChatGptAnswerFromThread(options) {
3741
3824
  }
3742
3825
  const port = resolveCdpPort(options.port);
3743
3826
  const timeoutMs = Math.max(1_000, options.timeoutMs ?? 60_000);
3744
- const url = normalizeChatGptTargetUrl(options.targetUrl);
3827
+ const url = normalizeChatGptThreadUrl(options.targetUrl);
3745
3828
  const page = await findChatGptPage(port, 3_000);
3746
3829
  if (!page.ok || !page.page) {
3747
3830
  throw new ChatGptBrowserBlockerError(page.blocker ?? {
package/dist/cli-pro.js CHANGED
@@ -4,7 +4,7 @@ import { createServer as createNetServer } from "node:net";
4
4
  import path from "node:path";
5
5
  import { BrowserProcessInspectionError } from "./browser-process.js";
6
6
  import { buildDryRunBundle } from "./bundle.js";
7
- import { ChatGptBrowserBlockerError, DEFAULT_CDP_PORT, resolveCdpPort, resolveConversationToDelete, resolveProjectToDelete, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, formatModelMenuOption, listChatGptModelOptions, deleteChatGptConversation, endWedgedBrowser, browserRecoveryPlan, findWedgedBrowser, wedgedBrowserBlocker, deleteChatGptProject, listChatGptProjectsWithIds, listRecentChatGptConversations, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, openChatGptTab, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveBrowserWindowMode, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt, statusMeansBrowserDead, namesPro, sendWarningsFromError, destinationVerification, chatGptProjectIdFromUrl } from "./chatgpt-browser.js";
7
+ import { ChatGptBrowserBlockerError, DEFAULT_CDP_PORT, resolveCdpPort, resolveConversationToDelete, resolveProjectToDelete, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, formatModelMenuOption, listChatGptModelOptions, deleteChatGptConversation, endWedgedBrowser, browserRecoveryPlan, findWedgedBrowser, wedgedBrowserBlocker, deleteChatGptProject, listChatGptProjectsWithIds, listRecentChatGptConversations, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, openChatGptTab, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveBrowserWindowMode, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt, statusMeansBrowserDead, namesPro, sendWarningsFromError, destinationVerification, chatGptProjectIdFromUrl, chatStepNamedAsModel, normalizeChatGptThreadUrl } from "./chatgpt-browser.js";
8
8
  import { ASK_PRO_BOOLEAN_FLAGS, ASK_PRO_PREVIEW_VALUE_FLAGS, ASK_PRO_VALUE_FLAGS, assertHelpRequestArgs, assertNoExtraArgs, assertOnlyOptions, findHelpFlagIndexBeforePromptDelimiter, formatCliCommand, hasAskProDryRunMode, hasAskProMode, hasAskProSendMode, isHelpSubcommand, parseAskProArgs, printHelpIfRequested, readFlag, readPortFlag, readPositionalsWithOptions, readNonNegativeIntegerFlag, readPositiveIntegerFlag, readRepeatedFlag, resolveCwdFlag, resolveOptionalFileFlag, unknownSubcommandError } from "./cli-args.js";
9
9
  import { printProBrowserHelp, printProHelp } from "./cli-help.js";
10
10
  import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
@@ -19,6 +19,7 @@ import { readBridgeRoots } from "./registry.js";
19
19
  import { ProdexRequestIdSchema, SessionKeySchema } from "./schema.js";
20
20
  import { BridgeStore, MAX_FETCHABLE_RESULT_ARTIFACT_BYTES } from "./store.js";
21
21
  import { CLI_VERSION } from "./cli-help.js";
22
+ import { assertRepoRelativePath, assertResolvedRepoPathAllowed } from "./repo.js";
22
23
  import { PRODEX_ISSUE_REPO, buildIssueReport, fileGitHubIssue } from "./issue-report.js";
23
24
  function resolveBrowserProfileDirForLaunch(profileDir, launchCwd = process.cwd()) {
24
25
  const resolved = path.resolve(launchCwd, profileDir);
@@ -652,6 +653,9 @@ export async function runProCommand(rest, io, runCliFn) {
652
653
  if (!targetUrl) {
653
654
  throw new Error("pro browser recover requires --target-url <thread-url> - the ChatGPT conversation URL whose finished answer to recover (e.g. the thread from a send_timeout blocker).");
654
655
  }
656
+ // Checked here, before the send lock and the navigation it guards, and
657
+ // outside the catch below that dresses errors as browser failures.
658
+ normalizeChatGptThreadUrl(targetUrl);
655
659
  const recoverPort = readPortFlag(browserArgs, "--port");
656
660
  const recoverTimeoutMs = readPositiveIntegerFlag(browserArgs, "--timeout-ms");
657
661
  const recoverRequestIdRaw = readFlag(browserArgs, "--request-id");
@@ -1205,6 +1209,21 @@ export async function runAskProCommand(rest, io, beforeSend) {
1205
1209
  }
1206
1210
  return absolute;
1207
1211
  });
1212
+ // An upload leaves the machine, so it gets the same guard as --file and
1213
+ // repo_read_file: no .env, .git or key files, and no symlink out of the
1214
+ // repo. --attach used to check only "inside the repo and exists", so both
1215
+ // `--attach .env` and a link to ~/.ssh reached the upload (measured
1216
+ // 2026-10-26); MCP attach takes this route too.
1217
+ for (const [index, absolute] of attachments.entries()) {
1218
+ const repoPath = path.relative(targetCwd, absolute).split(path.sep).join("/");
1219
+ try {
1220
+ assertRepoRelativePath(repoPath);
1221
+ await assertResolvedRepoPathAllowed(targetCwd, absolute, repoPath);
1222
+ }
1223
+ catch (error) {
1224
+ throw new Error(`--attach "${readRepeatedFlag(parsedAskPro.optionArgs, "--attach")[index]}" refused: ${errorMessage(error)}.`, { cause: error });
1225
+ }
1226
+ }
1208
1227
  if (attachments.length > 0 && !hasSendMode) {
1209
1228
  throw new Error("--attach only applies when sending (`prodex pro browser ask`); the dry-run preview cannot upload files.");
1210
1229
  }
@@ -2626,7 +2645,9 @@ export function resolveSelectionAxes(input) {
2626
2645
  const { explicit } = input;
2627
2646
  const defaults = input.defaults;
2628
2647
  const reasoningAxisChosen = explicit.proMode !== undefined || explicit.effort !== undefined;
2629
- const explicitlyPro = explicit.model !== undefined && namesPro(explicit.model);
2648
+ // Pro, and any other Chat step given as a model, is a position of the same
2649
+ // slider a saved effort sets, so asking for it answers the effort question.
2650
+ const explicitlyPro = explicit.model !== undefined && (namesPro(explicit.model) || chatStepNamedAsModel(explicit.model) !== undefined);
2630
2651
  const model = explicit.model ?? (explicit.effort !== undefined ? undefined : defaults?.model);
2631
2652
  const proMode = explicit.proMode ?? (reasoningAxisChosen ? undefined : defaults?.pro_mode);
2632
2653
  const effort = explicit.effort ?? (reasoningAxisChosen || explicitlyPro ? undefined : defaults?.effort);
@@ -174,7 +174,7 @@ prodex pro browser models
174
174
  ```
175
175
 
176
176
  - `--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro` sets the rung. English aliases `instant`/`light`, `medium`, `high`, `extrahigh`/`max`, `ultra` are accepted, and both the Korean and the English (US) ChatGPT labels are matched. `Max` and `Ultra` belong to the Work surface's ladder and apply only when the browser is already on Work; every other value is sent on Chat, whose top step is Pro.
177
- - `--model Pro` reaches the same top rung. The model rows in the picker (Latest, GPT-5.6 Sol, GPT-5.5) refuse automation clicks in the current UI - they carry `pointer-events: none` - so `--model` with any other label reports `model_not_applied` rather than pretending. On a display language other than Korean or English, pass the exact label `models` prints.
177
+ - `--model Pro` reaches the same top rung, and so does any other Chat step given as a model (`--model Instant`, `--model High`): it is applied to the slider with a `model_is_now_an_effort` warning. Max and Ultra are not, because they are Work rungs. The model rows in the picker (Latest, GPT-5.6 Sol, GPT-5.5) refuse automation clicks in the current UI - they carry `pointer-events: none` - so `--model` with any other label reports `model_not_applied` rather than pretending. On a display language other than Korean or English, pass the exact label `models` prints.
178
178
  - `--pro-mode 기본|확장` selects a Pro sub-mode where the picker still exposes one (the GPT-5.5 generation); with a single Pro rung it fails with guidance. `--pro-mode` and `--effort` are different axes of the same control and cannot be combined. Any Pro selection raises the default `--timeout-ms` to 1200000, because Pro reasoning routinely runs for many minutes; an explicit `--timeout-ms` always wins.
179
179
  - ChatGPT keeps two surfaces, Chat and Work, with different pickers; Work's ladder has no Pro. A send puts the browser back on Chat first and notes it on the receipt, so a browser that drifted onto Work does not quietly send on the wrong picker.
180
180
  - `--project "name"` enters an existing sidebar project before sending. `--project-new "name"` creates a new project (sidebar 새 프로젝트 popover, committed with Enter) and sends inside it. Neither can be combined with `--target-url` (the project step would navigate away from the confirmed tab), and `--project-new` never comes from saved defaults - creating a project is always an explicit per-ask choice.
package/docs/releasing.md CHANGED
@@ -35,6 +35,57 @@ The release adds a guarded, opt-in transition and more accurate blocker reportin
35
35
  **Open**
36
36
  - A long-running headless browser ends up behind Cloudflare's check (also noted on WSL, where the Xvfb display is used instead). On macOS there is no virtual display, and a minimized window counts as hidden, so the remaining no-headless option there is a window kept behind others. Changing the headless preference is a design decision that was not made here.
37
37
 
38
+ ## 0.40.41 Release Record
39
+
40
+ Release commit `e0ea707` on `main` (cut after PRs #45-#47, last merge `6911f3e`); tag `v0.40.41`. Released ahead of the usual batch for PR #46, which withheld answers from requests that had posted.
41
+
42
+ **Publishing**
43
+ - `publish.yml` run 40274938156 passed all six platforms, then npm publish with SLSA v1 provenance and the GitHub Release (2026-10-26 05:34 UTC start).
44
+ - `npm run release:verify` passed on the release commit after a build. The release script now refuses to tag when the release section has duplicate lines or an `## Unreleased` section survives the rename; the GitHub Release lists the same three entries as `CHANGELOG.md`.
45
+
46
+ **Installed**
47
+ - WSL, M3, Mac mini: `prodex --version` reports 0.40.41. Running services were not restarted: no host stdio MCP servers on WSL and 1 on M3; the Mac mini had none.
48
+ - The maintainer watchdog checkout was pulled to `e0ea707` and rebuilt.
49
+
50
+ **Verified live on installed 0.40.41 (WSL)**
51
+ - `Reply with only OK. Run npm test first, then \`npm test\` again.`: `request_verified: yes` (0.40.40 refused it as `request_mismatch`). Before the fix, the 2026-10-24 thread whose two sends had been refused also matched when re-read with the fixed extraction.
52
+ - `--model Instant` in a repo with a saved Pro effort: applied to the slider with `model_is_now_an_effort`, answered in 9 s.
53
+ - `pro browser recover --target-url "https://chatgpt.com/c/abc def"`: refused before the send lock; the shared tab did not move.
54
+
55
+ **Browser container**
56
+ - `container/redesign-patches` `60a49da` ports #45, `5154330` ports #46 (applied by hand: the container's message helpers differ from `main`), `6a9ea08` ports #47. Branch suite at `6a9ea08`: 2080 passed, 3 skipped; `release:verify` ok.
57
+ - WSL images `1ce5bcde0ace` (#45, `--model Instant` applied live), `88bd2a8f427a` (#46, the backtick repro verified), `e83a90389d02` (#47, bad URL refused, smoke `requestVerified: true`).
58
+ - M3 (colima `prodex-check`) images `c289e00e756d`, `ee6f98e1588d` (backtick repro verified), `5cd5a5642917` (bad URL refused, smoke ok). Every swap waited for an idle send lock.
59
+
60
+ **Incidents**
61
+ - 2026-10-22: the WSL disk filled to 100% from another project's session scratch data, so command output stopped being written mid-port. Nothing in prodex was involved; the #45 container port resumed after that session's data was cleaned up on its side.
62
+ - 2026-10-26: probing recovery input with `--target-url "https://chatgpt.com/c/abc def"` on 0.40.40 moved the shared ChatGPT tab to that URL (the bug PR #47 fixes); no send was in flight and the tab was moved back to the home page.
63
+
64
+ ## 0.40.40 Release Record
65
+
66
+ Release commit `3d671f5` on `main` (cut after PRs #38-#44, last merge `5609f4c`); tag `v0.40.40`.
67
+
68
+ **Publishing**
69
+ - `publish.yml` run 39563515199 passed all six platforms, then npm publish with SLSA v1 provenance and the GitHub Release (2026-10-21 07:39 UTC start).
70
+ - `npm run release:verify` passed on the release commit after a build.
71
+
72
+ **Changelog defect**
73
+ - The released `CHANGELOG.md` (also inside the npm tarball) repeats five entries in the 0.40.40 section and leaves the token TTL fix (#44) under a stray `## Unreleased` heading below it. Cause: the script that resolved CHANGELOG conflicts while rebasing queued PRs copied the branch's whole Unreleased section, which already held the entries merged before it, and #44's branch added a second Unreleased section. `main` was corrected right after the release, and the GitHub Release notes were replaced with the corrected eight entries. The npm tarball cannot be changed. The rebase script now carries only lines missing from `main` and stops on duplicate lines or a second Unreleased section; the release script stops if an Unreleased section survives the rename.
74
+
75
+ **Installed**
76
+ - WSL, M3, Mac mini: `prodex --version` reports 0.40.40. Running services were not restarted: 9 stdio MCP servers on WSL and 1 on M3 keep older code until their clients reconnect. The Mac mini had none running.
77
+ - The maintainer watchdog checkout was pulled to `3d671f5` and rebuilt.
78
+
79
+ **Verified live**
80
+ - Installed 0.40.40 on WSL: a prompt with `[the docs](https://en.wikipedia.org/wiki/Function_(mathematics))`, refused twice as `request_mismatch` by 0.40.39, returned `done` with `request_verified: yes`.
81
+ - Over MCP on installed 0.40.40: `repo_search "("` explains that the query is a regular expression.
82
+ - `pro report-issue` preview shows `blocked at` and `prodex filing this report`; `pro ask --tool web-search` names the send command.
83
+
84
+ **Browser container**
85
+ - `container/redesign-patches` `1f4baa6` ports PRs #38-#41 and `5812053` ports PRs #42-#44. Branch suite at `5812053`: 2074 passed, 3 skipped; `release:verify` ok. One full run failed `project-sidebar-current-ui` "retries a covered target three times" (untouched; passed 3/3 alone and on the rerun).
86
+ - WSL: image `75828cfb7ed5` after #38-#41 (the autolink prompt went through live with `request_verified: yes`), then `6042260bebe7` after #42-#44 (smoke `requestVerified: true`). Both swapped under the send lock check.
87
+ - M3 (colima `prodex-check`): image `59fd7b2f0e89`, then `2ab687763354`; smoke `requestVerified: true` after each swap, under the send lock check.
88
+
38
89
  ## 0.40.39 Release Record
39
90
 
40
91
  Release commit `c8a170f` on `main` (cut after PRs #33-#37, last merge `09ad42e`); tag `v0.40.39`. First batched release: five fixes merged on green CI and released together.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.40.40",
3
+ "version": "0.40.42",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",