sneakoscope 5.6.1 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  Sneakoscope Codex (`sks`) is a Codex CLI and Codex App harness for people who want parallel AI coding without losing proof. It gives Codex a simple front door, a dynamic worker swarm, a local dashboard, TriWiki project memory, and release gates that separate machine evidence from LLM opinion.
16
16
 
17
- Current release: `5.6.0`. New in this release: a source-cited TriWiki code index/pack (`sks wiki refresh --code`) so LLMs use fewer tokens navigating a codebase, native repair for Codex Desktop's Computer Use and Browser Use features, and an agent bridge (`sks mcp-server`, `SKS_AGENT_MODE`, NDJSON `--stream`) so any agent system not a specific one can drive SKS's full command surface. See [docs/AGENT-BRIDGE.md](docs/AGENT-BRIDGE.md) and [CHANGELOG.md](CHANGELOG.md).
17
+ Current release: SKS **5.7.0**. New in this release: repaired Codex App menu bar persistence and Fast Mode actions, codex-lb Fast Mode defaults pinned to the required current Codex model, legacy upgrade repair through `sks update` and `sks doctor --fix`, and a safer `npm publish --ignore-scripts` wrapper that runs the release gates before lifecycle-disabled publish. See [CHANGELOG.md](CHANGELOG.md).
18
18
 
19
19
  ## Install
20
20
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "5.6.1"
79
+ version = "5.7.0"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "5.6.1"
3
+ version = "5.7.0"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
4
4
  fn main() {
5
5
  let mut args = std::env::args().skip(1);
6
6
  match args.next().as_deref() {
7
- Some("--version") => println!("sks-rs 5.6.1"),
7
+ Some("--version") => println!("sks-rs 5.7.0"),
8
8
  Some("compact-info") => {
9
9
  let mut input = String::new();
10
10
  let _ = io::stdin().read_to_string(&mut input);
package/dist/bin/sks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '5.6.1';
2
+ const FAST_PACKAGE_VERSION = '5.7.0';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--agent' && args[1] === 'worker') {
@@ -143,7 +143,7 @@ export const COMMANDS = {
143
143
  'mad-db': routeStateMutator(entry('beta', 'Deprecated alias for MAD-SKS SQL-plane execution; redirects to sks mad-sks sql|apply-migration', 'dist/commands/mad-db.js', directCommand(() => import('../commands/mad-db.js'), 'dist/commands/mad-db.js')), ['mad-sks-gate.json']),
144
144
  'auto-review': entry('beta', 'Manage auto-review profile', 'dist/commands/auto-review.js', directCommand(() => import('../commands/auto-review.js'), 'dist/commands/auto-review.js')),
145
145
  'dollar-commands': entry('stable', 'List Codex App dollar commands', 'dist/core/commands/basic-cli.js', basicArgs('dollarCommandsCommand')),
146
- 'fast-mode': entry('stable', 'Toggle SKS Fast mode default for dollar-command routes', 'dist/core/commands/fast-mode-command.js', argsCommand(() => import('../core/commands/fast-mode-command.js'), 'fastModeCommand', 'dist/core/commands/fast-mode-command.js')),
146
+ 'fast-mode': skipMigrationGate(entry('stable', 'Toggle SKS Fast mode default for dollar-command routes', 'dist/core/commands/fast-mode-command.js', argsCommand(() => import('../core/commands/fast-mode-command.js'), 'fastModeCommand', 'dist/core/commands/fast-mode-command.js'))),
147
147
  commit: entry('stable', 'Create a simple git commit', 'dist/commands/commit.js', directCommand(() => import('../commands/commit.js'), 'dist/commands/commit.js')),
148
148
  'commit-and-push': entry('stable', 'Create a simple git commit and push', 'dist/commands/commit-and-push.js', directCommand(() => import('../commands/commit-and-push.js'), 'dist/commands/commit-and-push.js')),
149
149
  dfix: routeStateMutator(entry('stable', 'Run DFix diagnose/plan/patch/verify loop', 'dist/core/commands/dfix-command.js', commandArgsCommand(() => import('../core/commands/dfix-command.js'), 'dfixCommand', 'dist/core/commands/dfix-command.js')), ['dfix-gate.json']),
@@ -23,6 +23,7 @@ import { extractTomlTable, writeCodexConfigGuarded } from '../core/codex/codex-c
23
23
  import { cleanupCodexConfigBackups, validateCodexConfigRoundTrip } from '../core/codex/codex-config-toml.js';
24
24
  import { runPostinstallGlobalDoctorAndMarkPending } from '../core/update/update-migration-state.js';
25
25
  import { repairCodexImagegen } from '../core/doctor/imagegen-repair.js';
26
+ import { REQUIRED_CODEX_MODEL } from '../core/codex-model-guard.js';
26
27
  const CODEX_LB_PROVIDER_NAME = 'openai';
27
28
  const CODEX_LB_PROVIDER_ENV_KEY = 'CODEX_LB_API_KEY';
28
29
  const CODEX_LB_CANONICAL_FAST_SERVICE_TIER = 'priority';
@@ -275,7 +276,7 @@ async function postinstallHarnessConflictNotice(conflictScan) {
275
276
  console.log(formatHarnessConflictReport(conflictScan, { includePrompt: false }));
276
277
  console.log('\nWhat this means: npm can finish installing the package, but `sks setup` and `sks doctor --fix` will refuse to activate SKS until the conflicting harness is removed with human approval.');
277
278
  console.log('No files were removed by postinstall.');
278
- console.log('Cleanup requires a human-approved Codex App session. Recommended model: GPT-5.5, reasoning: high.');
279
+ console.log(`Cleanup requires a human-approved Codex App session. Recommended model: ${REQUIRED_CODEX_MODEL}, reasoning: high.`);
279
280
  if (shouldAskPostinstallQuestion()) {
280
281
  const answer = await askPostinstallQuestion('Show the cleanup prompt now? [y/N] ');
281
282
  if (/^(y|yes|예|네|응)$/i.test(answer.trim())) {
@@ -901,7 +902,7 @@ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
901
902
  const fetchImpl = opts.fetch || globalThis.fetch;
902
903
  if (typeof fetchImpl !== 'function')
903
904
  return { ok: true, status: 'skipped', skipped: true, reason: 'fetch unavailable' };
904
- const model = opts.model || env.SKS_CODEX_MODEL || 'gpt-5.5';
905
+ const model = opts.model || env.SKS_CODEX_MODEL || REQUIRED_CODEX_MODEL;
905
906
  const timeoutMs = Number(opts.timeoutMs || env.SKS_CODEX_LB_CHAIN_CHECK_TIMEOUT_MS || 8000);
906
907
  const serviceTier = opts.fastMode === true || opts.serviceTier === 'fast' || opts.serviceTier === CODEX_LB_CANONICAL_FAST_SERVICE_TIER
907
908
  ? CODEX_LB_CANONICAL_FAST_SERVICE_TIER
@@ -1890,8 +1891,9 @@ export function normalizeCodexFastModeUiConfig(text = '', opts = {}) {
1890
1891
  return normalizeCodexFastModeUiConfigOnce(normalizeCodexFastModeUiConfigOnce(text, opts), opts);
1891
1892
  }
1892
1893
  function normalizeCodexFastModeUiConfigOnce(text = '', opts = {}) {
1893
- // Keep model and reasoning selection out of top-level config so Codex Desktop can
1894
- // expose its native model/speed selectors. SKS-owned defaults live in profiles below.
1894
+ // Keep SKS-owned model/reasoning defaults out of top-level config so Codex
1895
+ // Desktop can expose its native selectors, but preserve a user's deliberate
1896
+ // top-level model/service/reasoning choices across install, update, and doctor.
1895
1897
  let next = String(text || '');
1896
1898
  const misplacedDefaultProfile = tomlTableString(next, 'user.fast_mode', 'default_profile');
1897
1899
  next = removeLegacyTopLevelCodexModeLocks(next);
@@ -1940,7 +1942,7 @@ function normalizeCodexFastModeUiConfigOnce(text = '', opts = {}) {
1940
1942
  // they are managed as per-file `<name>.config.toml` overlays by
1941
1943
  // migrateSksProfilesToPerFile (src/core/auto-review.ts), which also writes the
1942
1944
  // sks-fast-high overlay for CLI `--profile` use.
1943
- next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model = "gpt-5.5"');
1945
+ next = upsertTomlTableKey(next, 'profiles.sks-fast-high', `model = "${REQUIRED_CODEX_MODEL}"`);
1944
1946
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'service_tier = "fast"');
1945
1947
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'approval_policy = "on-request"');
1946
1948
  // Do not force a sandbox from the Codex App fast profile. The App/IDE
@@ -1966,9 +1968,13 @@ function removeLegacyTopLevelCodexModeLocks(text = '') {
1966
1968
  const lines = String(text || '').split('\n');
1967
1969
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
1968
1970
  const end = firstTable === -1 ? lines.length : firstTable;
1971
+ const topLevelModel = topLevelTomlString(text, 'model');
1972
+ const removeSksOwnedModeLock = topLevelModel === REQUIRED_CODEX_MODEL;
1969
1973
  return lines.filter((line, index) => {
1970
1974
  if (index >= end)
1971
1975
  return true;
1976
+ if (!removeSksOwnedModeLock)
1977
+ return true;
1972
1978
  return !/^\s*(?:model|model_reasoning_effort)\s*=/.test(line);
1973
1979
  }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
1974
1980
  }
@@ -2142,7 +2148,7 @@ export function codexFastModeDesktopStatus(text = '') {
2142
2148
  const profile = validation.parsed?.profiles?.['sks-fast-high'];
2143
2149
  const globalOn = validation.ok
2144
2150
  && validation.default_profile === 'sks-fast-high'
2145
- && profile?.model === 'gpt-5.5'
2151
+ && profile?.model === REQUIRED_CODEX_MODEL
2146
2152
  && profile?.service_tier === 'fast';
2147
2153
  return {
2148
2154
  schema: 'sks.codex-fast-mode-desktop-status.v1',
@@ -2862,7 +2868,7 @@ export async function selftestCodexLb(tmp) {
2862
2868
  // escape that dash emits literally and bash collapses to `"`).
2863
2869
  await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\"auth_mode\":\"apikey\",\"OPENAI_API_KEY\":\"%s\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
2864
2870
  await fsp.chmod(codexLbFakeCodex, 0o755);
2865
- await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "low"\nservice_tier = "fast"\nsuppress_unstable_features_warning = true\n\n[profiles.custom]\nmodel_reasoning_effort = "low"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\nhooks = true\n');
2871
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `model = "${REQUIRED_CODEX_MODEL}"\nmodel_reasoning_effort = "low"\nservice_tier = "fast"\nsuppress_unstable_features_warning = true\n\n[profiles.custom]\nmodel_reasoning_effort = "low"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\nhooks = true\n`);
2866
2872
  const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}`, SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' };
2867
2873
  const codexLbSetup = await runProcess(process.execPath, [packagedSksEntrypoint(), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
2868
2874
  cwd: tmp,
@@ -2967,7 +2973,7 @@ export async function selftestCodexLb(tmp) {
2967
2973
  await postinstall({
2968
2974
  bootstrap: async () => {
2969
2975
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
2970
- await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\nsuppress_unstable_features_warning = true\n\n[features]\nhooks = true\n');
2976
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `model = "${REQUIRED_CODEX_MODEL}"\nservice_tier = "fast"\nsuppress_unstable_features_warning = true\n\n[features]\nhooks = true\n`);
2971
2977
  }
2972
2978
  });
2973
2979
  }
@@ -2993,7 +2999,7 @@ export async function selftestCodexLb(tmp) {
2993
2999
  await writeTextAtomic(path.join(doctorProject, 'package.json'), '{"name":"codex-lb-doctor-project","version":"0.0.0"}\n');
2994
3000
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\nexport CODEX_LB_API_KEY='sk-test'\n");
2995
3001
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
2996
- await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\nsuppress_unstable_features_warning = true\n\n[features]\nhooks = true\n');
3002
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `model = "${REQUIRED_CODEX_MODEL}"\nservice_tier = "fast"\nsuppress_unstable_features_warning = true\n\n[features]\nhooks = true\n`);
2997
3003
  const codexLbDoctorRepair = await runProcess(process.execPath, [packagedSksEntrypoint(), 'doctor', '--fix', '--json'], {
2998
3004
  cwd: doctorProject,
2999
3005
  env: { ...codexLbEnvForSelftest, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-doctor-global') },
@@ -3186,7 +3192,7 @@ export async function selftestCodexLb(tmp) {
3186
3192
  if (!codexLbLegacyDoctorJson.repair?.codex_lb?.ok || !codexLbLegacyDoctorJson.repair.codex_lb.legacy_auth_migrated || !codexLbLegacyDoctorEnv.includes("CODEX_LB_API_KEY='sk-legacy-doctor'") || !codexLbLegacyDoctorAuth.includes('"auth_mode":"apikey"') || !codexLbLegacyDoctorAuth.includes('sk-legacy-doctor') || !hasTopLevelCodexLbSelected(codexLbLegacyDoctorConfig) || !codexLbLegacyDoctorConfig.includes('env_key = "CODEX_LB_API_KEY"'))
3187
3193
  throw new Error('selftest: legacy doctor codex-lb restore');
3188
3194
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\n");
3189
- await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
3195
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `model = "${REQUIRED_CODEX_MODEL}"\nservice_tier = "fast"\n`);
3190
3196
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-env-only"}\n');
3191
3197
  const codexLbLoginCallsBeforeEnvOnlyPostinstall = await codexLbLoginCallCount(codexLbHome);
3192
3198
  const codexLbEnvOnlyPostinstall = await runProcess(process.execPath, [packagedSksEntrypoint(), 'postinstall'], {
@@ -3204,7 +3210,7 @@ export async function selftestCodexLb(tmp) {
3204
3210
  if (!String(codexLbEnvOnlyPostinstall.stdout || '').includes('codex-lb auth: restored from existing Codex login cache') || !codexLbEnvOnlyPostinstallEnv.includes("CODEX_LB_API_KEY='sk-env-only'") || !codexLbEnvOnlyPostinstallConfig.includes('env_key = "CODEX_LB_API_KEY"') || !hasTopLevelCodexLbSelected(codexLbEnvOnlyPostinstallConfig) || !codexLbEnvOnlyPostinstallAuth.includes('sk-env-only') || codexLbLoginCallsAfterEnvOnlyPostinstall !== codexLbLoginCallsBeforeEnvOnlyPostinstall)
3205
3211
  throw new Error('selftest: env-only codex-lb postinstall restore');
3206
3212
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\n");
3207
- await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
3213
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `model = "${REQUIRED_CODEX_MODEL}"\nservice_tier = "fast"\n`);
3208
3214
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"apikey","key":"sk-env-only-doctor"}\n');
3209
3215
  const codexLbEnvOnlyDoctorProject = tmpdir();
3210
3216
  await ensureDir(path.join(codexLbEnvOnlyDoctorProject, '.git'));
@@ -3334,7 +3340,7 @@ export async function selftestCodexLb(tmp) {
3334
3340
  }
3335
3341
  if (autobypassLaunch.status !== 'chain_unhealthy' || autobypassLaunch.bypass_codex_lb !== true || autobypassLaunch.chain_health?.status !== 'second_request_failed')
3336
3342
  throw new Error('selftest: SKS_CODEX_LB_AUTOBYPASS=1 should bypass codex-lb on hard chain failure');
3337
- await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
3343
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `model = "${REQUIRED_CODEX_MODEL}"\nservice_tier = "fast"\n`);
3338
3344
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\nexport CODEX_LB_API_KEY='sk-test'\n");
3339
3345
  const missingProviderLaunchCalls = [];
3340
3346
  const missingProviderLaunch = await maybePromptCodexLbSetupForLaunch([], {
@@ -3395,11 +3401,11 @@ export async function selftestCodexLb(tmp) {
3395
3401
  throw new Error('selftest: codex-lb setup did not preserve Codex App feature flags, default plugins, profile-scoped reasoning effort, explicit Fast profile, Codex Git commit generation, or migrate the hooks feature flag');
3396
3402
  if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig))
3397
3403
  throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
3398
- const codexLbLaunch = `source ${path.join(tmp, '.codex', 'sks-codex-lb.env')} && codex '--model' 'gpt-5.5'`;
3404
+ const codexLbLaunch = `source ${path.join(tmp, '.codex', 'sks-codex-lb.env')} && codex '--model' '${REQUIRED_CODEX_MODEL}'`;
3399
3405
  if (!codexLbLaunch.includes('sks-codex-lb.env'))
3400
3406
  throw new Error('selftest: Zellij launch command does not source codex-lb env file');
3401
- if (!codexLbLaunch.includes("'--model' 'gpt-5.5'"))
3402
- throw new Error('selftest: Zellij launch command without args did not force GPT-5.5');
3407
+ if (!codexLbLaunch.includes(`'--model' '${REQUIRED_CODEX_MODEL}'`))
3408
+ throw new Error(`selftest: Zellij launch command without args did not force ${REQUIRED_CODEX_MODEL}`);
3403
3409
  const madLaunchSource = await safeReadText(path.join(packageRoot(), 'src', 'core', 'commands', 'mad-sks-command.js'));
3404
3410
  if (!madLaunchSource.includes('const lb = await deps.maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb') || !madLaunchSource.includes('bypass_codex_lb') || !madLaunchSource.includes('model_provider="openai"') || !madLaunchSource.includes('codexLbFreshSession: true'))
3405
3411
  throw new Error('selftest: MAD launch does not sync codex-lb auth and fresh-session launch options');
@@ -41,7 +41,7 @@ import { runDoctorFixTransaction } from '../core/doctor/doctor-transaction.js';
41
41
  import { planDoctorDirtyRepair } from '../core/doctor/doctor-dirty-planner.js';
42
42
  import { doctorRepairPostcheck } from '../core/doctor/doctor-repair-postcheck.js';
43
43
  import { withSecretPreservationGuard } from '../core/config/config-migration-journal.js';
44
- import { writeProjectUpdateMigrationReceipt } from '../core/update/update-migration-state.js';
44
+ import { isUpdateMigrationReceiptCurrent, projectUpdateMigrationReceiptPath, writeProjectUpdateMigrationReceipt } from '../core/update/update-migration-state.js';
45
45
  import { inspectSksMenuBarStatus, installSksMenuBar } from '../core/codex-app/sks-menubar.js';
46
46
  import { sweepSksTempDirs } from '../core/retention.js';
47
47
  import { reconcileSkills } from '../core/init/skills.js';
@@ -84,16 +84,7 @@ async function runDoctor(args = [], root, doctorFix) {
84
84
  const doctorPhaseIds = doctorPhaseIdsForProfile(doctorProfile);
85
85
  const doctorDirtyPlan = doctorFix ? planDoctorDirtyRepair(root, doctorPhaseIds) : null;
86
86
  let setupRepair = null;
87
- const sksUpdate = doctorFix
88
- ? {
89
- schema: 'sks.update-now.v2',
90
- ok: true,
91
- status: 'skipped',
92
- reason: 'manual_update_commands_only',
93
- stages: [],
94
- migration_current: true
95
- }
96
- : null;
87
+ let sksUpdate = null;
97
88
  let migrationPreFix = null;
98
89
  if (doctorFix) {
99
90
  // Snapshot config content before ANY mutation so the migration journal can
@@ -117,7 +108,7 @@ async function runDoctor(args = [], root, doctorFix) {
117
108
  // backs up + parse-validates before writing, no-op when already present.
118
109
  codex_app_fast_mode: flag(args, '--local-only')
119
110
  ? { status: 'skipped', reason: 'local-only repair' }
120
- : deepDiagnostics ? await ensureGlobalCodexFastModeDuringInstall().catch((err) => ({ status: 'failed', error: err?.message || String(err) })) : { status: 'skipped', reason: 'default_doctor_no_global_fast_mode_regeneration' }
111
+ : await ensureGlobalCodexFastModeDuringInstall().catch((err) => ({ status: 'failed', error: err?.message || String(err) }))
121
112
  };
122
113
  }
123
114
  const skillsReconcile = doctorFix
@@ -823,16 +814,47 @@ async function runDoctor(args = [], root, doctorFix) {
823
814
  ...(pluginPolicy?.doctor_warnings || [])
824
815
  ]
825
816
  });
826
- if (doctorFix && ready.ready === true) {
827
- await writeProjectUpdateMigrationReceipt({
828
- root,
829
- source: `doctor-${doctorProfile}`,
830
- blockers: [],
831
- warnings: [
832
- ...(doctorNativeCapabilityRepair?.optional_warnings || []),
833
- ...(doctorFixPostcheck?.optional_warnings || [])
834
- ]
835
- }).catch(() => undefined);
817
+ if (doctorFix) {
818
+ const readinessBlockers = Array.isArray(ready.blockers) ? ready.blockers.map(String).filter(Boolean) : [];
819
+ const migrationWarnings = [
820
+ ...(doctorNativeCapabilityRepair?.optional_warnings || []),
821
+ ...(doctorFixPostcheck?.optional_warnings || [])
822
+ ];
823
+ try {
824
+ const receiptInput = {
825
+ root,
826
+ source: `doctor-${doctorProfile}`,
827
+ blockers: readinessBlockers,
828
+ warnings: migrationWarnings
829
+ };
830
+ if (readinessBlockers.length)
831
+ receiptInput.status = 'blocked';
832
+ const receipt = await writeProjectUpdateMigrationReceipt(receiptInput);
833
+ sksUpdate = {
834
+ schema: 'sks.update-now.v2',
835
+ ok: receipt.status === 'current' && isUpdateMigrationReceiptCurrent(receipt),
836
+ status: receipt.status === 'current' ? 'repaired' : receipt.status,
837
+ reason: receipt.status === 'current' ? 'doctor_fix_wrote_current_project_migration_receipt' : 'doctor_fix_migration_receipt_blocked',
838
+ stages: receipt.legacy_migration_stages || [],
839
+ migration_current: isUpdateMigrationReceiptCurrent(receipt),
840
+ receipt_path: projectUpdateMigrationReceiptPath(root),
841
+ blockers: receipt.blockers || [],
842
+ warnings: receipt.warnings || []
843
+ };
844
+ }
845
+ catch (err) {
846
+ sksUpdate = {
847
+ schema: 'sks.update-now.v2',
848
+ ok: false,
849
+ status: 'blocked',
850
+ reason: 'doctor_fix_migration_receipt_failed',
851
+ stages: [],
852
+ migration_current: false,
853
+ receipt_path: projectUpdateMigrationReceiptPath(root),
854
+ blockers: [`migration_receipt_failed:${err?.message || String(err)}`],
855
+ warnings: migrationWarnings
856
+ };
857
+ }
836
858
  }
837
859
  const zellijReadiness = buildZellijReadiness(root, zellij, ready);
838
860
  const runtimeReadiness = buildRuntimeReadiness(zellijReadiness, codexNativeFeatureMatrix);