sneakoscope 6.1.2 → 6.2.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.
Files changed (64) hide show
  1. package/README.md +11 -4
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/install-helpers.js +8 -3
  5. package/dist/config/skills-manifest.json +58 -58
  6. package/dist/core/agents/agent-effort-policy.js +28 -17
  7. package/dist/core/agents/agent-schema.js +1 -1
  8. package/dist/core/codex/codex-config-guard.js +178 -7
  9. package/dist/core/codex/codex-config-readability.js +21 -8
  10. package/dist/core/codex/codex-config-toml.js +14 -11
  11. package/dist/core/codex-app/mcp-manager.js +679 -0
  12. package/dist/core/codex-app/sks-menubar.js +405 -6
  13. package/dist/core/codex-control/codex-lb-launch-recovery.js +15 -0
  14. package/dist/core/codex-native/core-skill-manifest.js +1 -1
  15. package/dist/core/commands/mad-sks-command.js +44 -3
  16. package/dist/core/commands/menubar-command.js +94 -0
  17. package/dist/core/commands/naruto-command.js +3 -1
  18. package/dist/core/commands/wiki-command.js +11 -5
  19. package/dist/core/fsx.js +1 -0
  20. package/dist/core/hooks-runtime/code-pack-freshness-preflight.js +14 -8
  21. package/dist/core/hooks-runtime/naruto-decision-gate.js +184 -0
  22. package/dist/core/hooks-runtime/stop-repeat-guard.js +89 -0
  23. package/dist/core/hooks-runtime.js +36 -105
  24. package/dist/core/init/skills.js +4 -4
  25. package/dist/core/init.js +1 -1
  26. package/dist/core/managed-assets/managed-assets-manifest.js +178 -29
  27. package/dist/core/preflight/parallel-preflight-engine.js +16 -3
  28. package/dist/core/proof/route-adapter.js +1 -1
  29. package/dist/core/proof/selftest-proof-fixtures.js +3 -5
  30. package/dist/core/provider/model-router.js +15 -6
  31. package/dist/core/release/package-size-budget.js +4 -6
  32. package/dist/core/research/research-plan-markdown.js +123 -0
  33. package/dist/core/research/research-super-search.js +8 -4
  34. package/dist/core/research.js +4 -119
  35. package/dist/core/retention.js +70 -2
  36. package/dist/core/routes/dollar-manifest-lite.js +1 -1
  37. package/dist/core/routes.js +75 -22
  38. package/dist/core/subagents/agent-catalog.js +87 -15
  39. package/dist/core/subagents/model-policy.js +189 -48
  40. package/dist/core/subagents/naruto-help-contract.js +11 -4
  41. package/dist/core/subagents/official-subagent-preparation.js +55 -9
  42. package/dist/core/subagents/official-subagent-prompt.js +45 -23
  43. package/dist/core/subagents/thread-budget.js +1 -1
  44. package/dist/core/subagents/triwiki-attention.js +117 -14
  45. package/dist/core/triwiki/code-pack-head-freshness.js +291 -0
  46. package/dist/core/version.js +1 -1
  47. package/dist/core/zellij/zellij-launcher.js +12 -2
  48. package/dist/core/zellij/zellij-pane-proof.js +9 -1
  49. package/dist/core/zellij/zellij-update.js +14 -1
  50. package/dist/scripts/canonical-test-runner.js +7 -1
  51. package/dist/scripts/codex-lb-fast-mode-truth-check.js +11 -1
  52. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +9 -2
  53. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +13 -2
  54. package/dist/scripts/codex-native-agent-role-content-check.js +13 -1
  55. package/dist/scripts/codex-sdk-backend-router-check.js +2 -0
  56. package/dist/scripts/lib/codex-sdk-gate-lib.js +4 -0
  57. package/dist/scripts/official-subagent-workflow-check.js +1 -1
  58. package/dist/scripts/packlist-performance-check.js +1 -18
  59. package/dist/scripts/python-codex-sdk-all-pipelines-check.js +8 -0
  60. package/dist/scripts/sks-menubar-install-check.js +6 -1
  61. package/dist/scripts/super-search-provider-interface-check.js +31 -18
  62. package/package.json +3 -1
  63. package/dist/scripts/codex-0139-feature-probes-check.js +0 -30
  64. package/dist/scripts/codex-0139-marketplace-source-check.js +0 -13
@@ -62,24 +62,31 @@ export function decideAgentEffort(input = {}) {
62
62
  ]
63
63
  };
64
64
  }
65
- // Official Codex subagents always use max reasoning. Clear bounded work uses
66
- // Luna; any judgment-bearing slice uses Sol. There is no automatic Terra or
67
- // lower-effort fallback.
65
+ // Official Codex subagents use one of four fixed profiles: Luna Max for tiny
66
+ // mechanical work, Sol High for ordinary implementation, Sol Max for
67
+ // judgment, and Terra Medium for long-context or Codex-tool execution.
68
68
  export function decideOfficialSubagentModel(input = {}) {
69
69
  const persona = input.persona || {};
70
70
  const prompt = String(input.prompt || '');
71
71
  const role = String(persona.role || '');
72
72
  const agentId = String(input.agentId || persona.id || 'subagent');
73
+ const routingRole = [agentId, persona.naruto_role, persona.work_kind, role].filter(Boolean).join(' ');
74
+ const taskClass = /(?:implementation_specialist|ui_implementer|native_app_specialist)/i.test(routingRole)
75
+ ? 'implementation'
76
+ : /(?:explorer|docs_maintainer|long_context_analyst|computer_use_operator|browser_use_operator|image_generation_operator)/i.test(routingRole)
77
+ ? 'context_tools'
78
+ : undefined;
73
79
  const routed = decideSubagentModel({
74
- title: [agentId, persona.naruto_role, persona.work_kind].filter(Boolean).join(' '),
80
+ title: routingRole,
75
81
  description: [prompt, persona.risk_focus, persona.write_policy].filter(Boolean).join(' '),
76
82
  role,
77
- expectedOutput: (persona.output_expectations || []).join(' ')
83
+ expectedOutput: (persona.output_expectations || []).join(' '),
84
+ ...(taskClass ? { taskClass } : {})
78
85
  });
79
- const effort = 'max';
86
+ const effort = routed.modelReasoningEffort;
80
87
  const modelCapability = codexModelEffortCapability({
81
88
  model: routed.model,
82
- advertisedEfforts: ['max'],
89
+ advertisedEfforts: [effort],
83
90
  defaultEffort: effort
84
91
  });
85
92
  return {
@@ -99,11 +106,15 @@ export function decideOfficialSubagentModel(input = {}) {
99
106
  reason: routed.reason,
100
107
  dynamic: true,
101
108
  escalation_triggers: [
102
- 'UI, review, debugging, planning, architecture, integration, security, database, release, or ambiguity selects Sol Max',
103
- 'requiresJudgment or reasoning-sensitive evidence selects Sol Max',
104
- 'requested model or max effort unavailable blocks instead of silently falling back'
109
+ 'review, debugging, planning, architecture, integration, security, database, research, release, or ambiguity selects Sol Max',
110
+ 'mixed tool-and-judgment work is split; an unsplittable slice selects Sol Max',
111
+ 'requested model/effort profile unavailable blocks instead of silently falling back'
105
112
  ],
106
- downshift_triggers: []
113
+ downshift_triggers: [
114
+ 'ordinary UI, logic, backend, or native implementation selects Sol High',
115
+ 'long-context, Browser/Chrome, Computer Use, or image-generation execution selects Terra Medium',
116
+ 'tiny short-context mechanical work selects Luna Max'
117
+ ]
107
118
  };
108
119
  }
109
120
  /** @deprecated since 6.1.1. Use decideOfficialSubagentModel. */
@@ -129,20 +140,20 @@ export function buildAgentEffortPolicy(roster = {}) {
129
140
  policy_version: 1,
130
141
  dynamic: true,
131
142
  service_tier: 'fast',
132
- model_catalog_policy: narutoFamilyOnly ? 'official_subagent_luna_sol_max' : 'codex_catalog_passthrough',
133
- model_constraint: narutoFamilyOnly ? ['gpt-5.6-luna', 'gpt-5.6-sol'] : null,
134
- explicit_compatibility_models: narutoFamilyOnly ? ['gpt-5.6-terra'] : null,
143
+ model_catalog_policy: narutoFamilyOnly ? 'official_subagent_four_profile_matrix' : 'codex_catalog_passthrough',
144
+ model_constraint: narutoFamilyOnly ? ['gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-5.6-sol'] : null,
145
+ explicit_compatibility_models: null,
135
146
  model_tiers: narutoFamilyOnly
136
- ? ['gpt-5.6-luna-max', 'gpt-5.6-sol-max', 'gpt-5.6-terra-max']
147
+ ? ['gpt-5.6-luna-max', 'gpt-5.6-sol-high', 'gpt-5.6-sol-max', 'gpt-5.6-terra-medium']
137
148
  : ['codex-selected-low', 'codex-selected-medium', 'codex-selected-high', 'codex-selected-xhigh', 'glm-5.2-minimal', 'glm-5.2-low', 'glm-5.2-high', 'glm-5.2-xhigh'],
138
- allowed_efforts: narutoFamilyOnly ? ['max'] : codexModelEffortCapability().advertised_efforts,
149
+ allowed_efforts: narutoFamilyOnly ? ['medium', 'high', 'max'] : codexModelEffortCapability().advertised_efforts,
139
150
  model_effort_capability: codexModelEffortCapability(),
140
151
  max_agents: roster.max_agents || 20,
141
152
  agent_count: roster.agent_count || decisions.length,
142
153
  concurrency: roster.concurrency || decisions.length,
143
154
  decisions,
144
155
  rule: narutoFamilyOnly
145
- ? 'Official Naruto subagents automatically use GPT-5.6 Luna Max for clear bounded repeatable work and GPT-5.6 Sol Max for every reasoning-sensitive slice; an explicit user-selected Terra model remains compatible but is never selected automatically.'
156
+ ? 'Official Naruto subagents use GPT-5.6 Luna Max only for tiny short-context mechanical work, GPT-5.6 Sol High for ordinary implementation, GPT-5.6 Sol Max for judgment-heavy work, and GPT-5.6 Terra Medium for long-context or Browser/Chrome, Computer Use, and image-generation execution. Judgment wins when one slice cannot be safely split.'
146
157
  : 'Codex/OpenAI workers inherit the current Codex-selected model, including future catalog entries; SKS changes only advertised reasoning effort. Explicit non-Codex provider modes retain their provider model.'
147
158
  };
148
159
  }
@@ -12,7 +12,7 @@ export const MAX_AGENT_COUNT = 20;
12
12
  export const DEFAULT_AGENT_CONCURRENCY = 4;
13
13
  /** @deprecated Legacy process-swarm queue ceiling; never an official thread cap. */
14
14
  export const MAX_NARUTO_AGENT_COUNT = 100;
15
- /** @deprecated Legacy clone default; official workflows use one safe default child unless --agents is explicit. */
15
+ /** @deprecated Legacy clone default; official workflows use two default children unless --agents is explicit. */
16
16
  export const DEFAULT_NARUTO_CLONES = 8;
17
17
  export const AGENT_BACKENDS = ['fake', 'process', 'codex-sdk', 'zellij', 'ollama', 'local-llm'];
18
18
  export function normalizeAgentBackend(input) {
@@ -1,4 +1,6 @@
1
1
  import path from 'node:path';
2
+ import fsp from 'node:fs/promises';
3
+ import { randomBytes } from 'node:crypto';
2
4
  import { appendJsonl, ensureDir, nowIso, readText, sha256, writeTextAtomic } from '../fsx.js';
3
5
  import { diffCodexAppUiSnapshots, snapshotCodexAppUiState } from '../codex-app/codex-app-ui-state-snapshot.js';
4
6
  import { cleanupCodexConfigBackups, validateCodexConfigRoundTrip } from './codex-config-toml.js';
@@ -38,10 +40,13 @@ export async function writeCodexConfigGuarded(input) {
38
40
  return result;
39
41
  }
40
42
  const beforeSnapshot = await snapshotForConfig(root, configPath).catch(() => null);
41
- let next = ensureTrailingNewline(await input.mutate(before));
43
+ const normalizeText = input.preserveTextFormatting === true
44
+ ? (value) => String(value || '')
45
+ : ensureTrailingNewline;
46
+ let next = normalizeText(await input.mutate(before));
42
47
  if (input.removeTopLevelModeLocks === true)
43
48
  next = removeLegacyTopLevelCodexModeLocks(next);
44
- const preserved = input.preserveFastUiKeys === false ? { text: ensureTrailingNewline(next), keys: [] } : mergeLostFastUiKeys(before, next);
49
+ const preserved = input.preserveFastUiKeys === false ? { text: normalizeText(next), keys: [] } : mergeLostFastUiKeys(before, next);
45
50
  next = preserved.text;
46
51
  if (input.removeTopLevelModeLocks === true)
47
52
  next = removeLegacyTopLevelCodexModeLocks(next);
@@ -63,8 +68,29 @@ export async function writeCodexConfigGuarded(input) {
63
68
  });
64
69
  return result;
65
70
  }
66
- if (next === ensureTrailingNewline(before)) {
67
- await writeTextAtomic(configPath, next, { mode: 0o600 });
71
+ const expectedBefore = normalizeText(before);
72
+ if (next === expectedBefore) {
73
+ if (input.verifyUnchangedBeforeWrite === true) {
74
+ const observed = await readConfigCommitSnapshot(configPath);
75
+ const expectedExists = input.expectedBeforeExists ?? true;
76
+ if (!configCommitSnapshotMatches(observed, expectedExists, before)) {
77
+ return recordConcurrentConfigChange({
78
+ root,
79
+ reportPath: input.reportPath,
80
+ cause,
81
+ configPath,
82
+ before,
83
+ expectedExists,
84
+ observed,
85
+ backupPath: null,
86
+ repairedKeys: preserved.keys,
87
+ forbiddenTopLevel
88
+ });
89
+ }
90
+ }
91
+ else {
92
+ await writeTextAtomic(configPath, next, { mode: 0o600 });
93
+ }
68
94
  const result = { ok: true, status: 'present', config_path: configPath, backup_path: null, changed: false, repaired_keys: preserved.keys, forbidden_top_level: forbiddenTopLevel };
69
95
  if (preserved.keys.length || forbiddenTopLevel.length) {
70
96
  await recordCodexConfigGuard(root, input.reportPath, {
@@ -79,9 +105,37 @@ export async function writeCodexConfigGuarded(input) {
79
105
  }
80
106
  return result;
81
107
  }
82
- const backupPath = before.trim() ? await backupCodexConfig(configPath, before, input.backupTag || cause) : null;
83
- await ensureDir(path.dirname(configPath));
84
- await writeTextAtomic(configPath, next, { mode: 0o600 });
108
+ let backupPath = null;
109
+ if (input.verifyUnchangedBeforeWrite === true) {
110
+ const expectedExists = input.expectedBeforeExists ?? true;
111
+ const commit = await commitCodexConfigIfUnchanged({
112
+ configPath,
113
+ before,
114
+ next,
115
+ expectedExists,
116
+ backupTag: input.backupTag || cause
117
+ });
118
+ if (!commit.ok) {
119
+ return recordConcurrentConfigChange({
120
+ root,
121
+ reportPath: input.reportPath,
122
+ cause,
123
+ configPath,
124
+ before,
125
+ expectedExists,
126
+ observed: commit.observed,
127
+ backupPath: commit.backupPath,
128
+ repairedKeys: preserved.keys,
129
+ forbiddenTopLevel
130
+ });
131
+ }
132
+ backupPath = commit.backupPath;
133
+ }
134
+ else {
135
+ backupPath = before.trim() ? await backupCodexConfig(configPath, before, input.backupTag || cause) : null;
136
+ await ensureDir(path.dirname(configPath));
137
+ await writeTextAtomic(configPath, next, { mode: 0o600 });
138
+ }
85
139
  const afterSnapshot = await snapshotForConfig(root, configPath).catch(() => null);
86
140
  const diff = beforeSnapshot && afterSnapshot ? diffCodexAppUiSnapshots(beforeSnapshot, afterSnapshot) : null;
87
141
  const result = {
@@ -115,6 +169,123 @@ export async function writeCodexConfigGuarded(input) {
115
169
  });
116
170
  return { ...result, report_path: reportPath };
117
171
  }
172
+ function configCommitSnapshotMatches(observed, expectedExists, expectedText) {
173
+ return observed.ok
174
+ && observed.exists === expectedExists
175
+ && sha256(observed.text) === sha256(expectedText);
176
+ }
177
+ async function recordConcurrentConfigChange(input) {
178
+ const result = {
179
+ ok: false,
180
+ status: 'concurrent_change_detected',
181
+ config_path: input.configPath,
182
+ backup_path: input.backupPath,
183
+ changed: false,
184
+ repaired_keys: input.repairedKeys,
185
+ forbidden_top_level: input.forbiddenTopLevel
186
+ };
187
+ await recordCodexConfigGuard(input.root, input.reportPath, {
188
+ cause: input.cause,
189
+ config_path: input.configPath,
190
+ ok: false,
191
+ status: result.status,
192
+ expected_exists: input.expectedExists,
193
+ observed_exists: input.observed.exists,
194
+ expected_sha256: sha256(input.before),
195
+ observed_sha256: input.observed.ok ? sha256(input.observed.text) : null,
196
+ observed_status: input.observed.status,
197
+ backup_path: input.backupPath,
198
+ changed: false
199
+ });
200
+ return result;
201
+ }
202
+ async function commitCodexConfigIfUnchanged(input) {
203
+ await ensureDir(path.dirname(input.configPath));
204
+ const token = `${Date.now().toString(36)}-${process.pid}-${randomBytes(6).toString('hex')}`;
205
+ const candidatePath = `${input.configPath}.sks-commit-${token}.tmp`;
206
+ let backupPath = null;
207
+ try {
208
+ await writeTextAtomic(candidatePath, input.next, { mode: 0o600 });
209
+ const observed = await readConfigCommitSnapshot(input.configPath);
210
+ if (!configCommitSnapshotMatches(observed, input.expectedExists, input.before)) {
211
+ return { ok: false, backupPath, observed };
212
+ }
213
+ if (input.expectedExists) {
214
+ backupPath = `${input.configPath}.sks-${safeBackupTag(input.backupTag)}-${token}.bak`;
215
+ try {
216
+ await fsp.rename(input.configPath, backupPath);
217
+ }
218
+ catch {
219
+ return { ok: false, backupPath: null, observed: await readConfigCommitSnapshot(input.configPath) };
220
+ }
221
+ const claimed = await readConfigCommitSnapshot(backupPath);
222
+ if (!configCommitSnapshotMatches(claimed, true, input.before)) {
223
+ await restoreClaimedConfigIfAbsent(backupPath, input.configPath, claimed);
224
+ return { ok: false, backupPath, observed: claimed };
225
+ }
226
+ try {
227
+ await fsp.chmod(backupPath, 0o600);
228
+ const now = new Date();
229
+ await fsp.utimes(backupPath, now, now);
230
+ }
231
+ catch {
232
+ await restoreClaimedConfigIfAbsent(backupPath, input.configPath, claimed);
233
+ return { ok: false, backupPath, observed: await readConfigCommitSnapshot(input.configPath) };
234
+ }
235
+ const afterClaim = await readConfigCommitSnapshot(input.configPath);
236
+ if (!afterClaim.ok || afterClaim.exists) {
237
+ if (!afterClaim.exists)
238
+ await restoreClaimedConfigIfAbsent(backupPath, input.configPath, claimed);
239
+ return { ok: false, backupPath, observed: await readConfigCommitSnapshot(input.configPath) };
240
+ }
241
+ }
242
+ try {
243
+ await fsp.link(candidatePath, input.configPath);
244
+ }
245
+ catch {
246
+ if (backupPath) {
247
+ const claimed = await readConfigCommitSnapshot(backupPath);
248
+ await restoreClaimedConfigIfAbsent(backupPath, input.configPath, claimed);
249
+ }
250
+ return { ok: false, backupPath, observed: await readConfigCommitSnapshot(input.configPath) };
251
+ }
252
+ await fsp.chmod(input.configPath, 0o600).catch(() => undefined);
253
+ await cleanupCodexConfigBackups(input.configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
254
+ return { ok: true, backupPath };
255
+ }
256
+ finally {
257
+ await fsp.rm(candidatePath, { force: true }).catch(() => undefined);
258
+ }
259
+ }
260
+ async function restoreClaimedConfigIfAbsent(backupPath, configPath, claimed) {
261
+ if (!claimed.ok || !claimed.exists || claimed.status !== 'regular')
262
+ return;
263
+ await fsp.link(backupPath, configPath).catch(() => undefined);
264
+ }
265
+ function safeBackupTag(value) {
266
+ return String(value || 'codex-config').replace(/[^A-Za-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 96) || 'codex-config';
267
+ }
268
+ async function readConfigCommitSnapshot(configPath) {
269
+ let stat;
270
+ try {
271
+ stat = await fsp.lstat(configPath);
272
+ }
273
+ catch (error) {
274
+ if (error?.code === 'ENOENT')
275
+ return { ok: true, exists: false, text: '', status: 'missing' };
276
+ return { ok: false, exists: false, text: '', status: 'read_failed' };
277
+ }
278
+ if (stat.isSymbolicLink())
279
+ return { ok: false, exists: true, text: '', status: 'symlink' };
280
+ if (!stat.isFile())
281
+ return { ok: false, exists: true, text: '', status: 'non_regular' };
282
+ try {
283
+ return { ok: true, exists: true, text: await fsp.readFile(configPath, 'utf8'), status: 'regular' };
284
+ }
285
+ catch {
286
+ return { ok: false, exists: true, text: '', status: 'read_failed' };
287
+ }
288
+ }
118
289
  export function extractTomlTable(text, tableName) {
119
290
  const source = String(text || '');
120
291
  const header = `[${tableName}]`;
@@ -50,16 +50,29 @@ export async function inspectCodexConfigReadability(rootInput = process.cwd(), o
50
50
  }
51
51
  add({ name: 'config_symlink', ok: !isSymlink || symlinkDetail.allowed === true, detail: symlinkDetail });
52
52
  }
53
- if (process.platform === 'darwin') {
54
- add(await commandCheck('macos_acl_ls_le', 'ls', ['-le', configPath], root));
55
- const acl = checks.find((check) => check.name === 'macos_acl_ls_le');
53
+ const readChecksPromise = Promise.all([
54
+ nodeReadCheck(configPath),
55
+ childReadCheck(configPath, root),
56
+ codexCliConfigLoadCheck(root, configPath, opts)
57
+ ]);
58
+ // These are independent read-only metadata probes. Running them serially
59
+ // multiplied a single TCC/ACL timeout by three on the MAD launch path.
60
+ const macChecksPromise = process.platform === 'darwin'
61
+ ? Promise.all([
62
+ commandCheck('macos_acl_ls_le', 'ls', ['-le', configPath], root),
63
+ commandCheck('macos_flags_ls_lO', 'ls', ['-lO', configPath], root),
64
+ commandCheck('macos_xattr', 'xattr', ['-l', configPath], root, { allowExitCodes: [0, 1] })
65
+ ])
66
+ : Promise.resolve(null);
67
+ const [[nodeRead, childRead, codexLoad], macChecks] = await Promise.all([readChecksPromise, macChecksPromise]);
68
+ if (macChecks) {
69
+ const [acl, flags, xattrs] = macChecks;
70
+ add(acl);
56
71
  if (/\bdeny\b.*\b(read|readattr|readextattr|readsecurity|search)\b/i.test(String(acl?.detail?.stdout || '')))
57
72
  blockers.add('acl_denied');
58
- const flags = await commandCheck('macos_flags_ls_lO', 'ls', ['-lO', configPath], root);
59
73
  add(flags);
60
74
  if (/\b(uchg|schg|restricted)\b/.test(String(flags.detail?.stdout || '')))
61
75
  blockers.add('flags_locked');
62
- const xattrs = await commandCheck('macos_xattr', 'xattr', ['-l', configPath], root, { allowExitCodes: [0, 1] });
63
76
  add(xattrs);
64
77
  add({ name: 'macos_quarantine_xattr', ok: !/com\.apple\.quarantine/.test(String(xattrs.detail?.stdout || '')), detail: { present: /com\.apple\.quarantine/.test(String(xattrs.detail?.stdout || '')) } });
65
78
  if (/com\.apple\.quarantine/.test(String(xattrs.detail?.stdout || '')))
@@ -68,9 +81,9 @@ export async function inspectCodexConfigReadability(rootInput = process.cwd(), o
68
81
  else {
69
82
  add({ name: 'macos_metadata', ok: true, status: 'skipped_non_macos' });
70
83
  }
71
- add(await nodeReadCheck(configPath));
72
- add(await childReadCheck(configPath, root));
73
- add(await codexCliConfigLoadCheck(root, configPath, opts));
84
+ add(nodeRead);
85
+ add(childRead);
86
+ add(codexLoad);
74
87
  }
75
88
  else {
76
89
  add({ name: 'config_file_checks', ok: true, status: 'skipped_config_missing', detail: { config_path: configPath } });
@@ -52,9 +52,8 @@ export async function cleanupCodexConfigBackups(configPath, opts = {}) {
52
52
  const now = Date.now();
53
53
  const groups = new Map();
54
54
  for (const file of entries) {
55
- if (!file.startsWith(`${base}.`))
56
- continue;
57
- if (!/(?:^|[.-])(?:sks-|bak-|struct-bak-)|\.bak$/.test(file))
55
+ const tag = ownedBackupTag(base, file);
56
+ if (!tag)
58
57
  continue;
59
58
  const abs = path.join(dir, file);
60
59
  let stat;
@@ -66,7 +65,6 @@ export async function cleanupCodexConfigBackups(configPath, opts = {}) {
66
65
  }
67
66
  if (!stat.isFile())
68
67
  continue;
69
- const tag = backupTag(base, file);
70
68
  const group = groups.get(tag) || [];
71
69
  group.push({ file, abs, mtimeMs: stat.mtimeMs });
72
70
  groups.set(tag, group);
@@ -87,16 +85,21 @@ export async function cleanupCodexConfigBackups(configPath, opts = {}) {
87
85
  }
88
86
  return { cleaned: removed.length, files: removed };
89
87
  }
90
- function backupTag(base, file) {
88
+ function ownedBackupTag(base, file) {
89
+ if (!file.startsWith(`${base}.`))
90
+ return null;
91
91
  const suffix = file.slice(`${base}.`.length);
92
- const sks = suffix.match(/^sks-([^-]+)-/);
93
- if (sks?.[1])
94
- return `sks-${sks[1]}`;
95
- if (suffix.startsWith('struct-bak-'))
92
+ if (/^sks-[A-Za-z0-9_.-]+\.bak$/.test(suffix)) {
93
+ const group = suffix.match(/^sks-([^-]+)-/)?.[1] || suffix.slice('sks-'.length, -'.bak'.length);
94
+ return `sks-${group}`;
95
+ }
96
+ if (/^struct-bak-[A-Za-z0-9_.-]+$/.test(suffix))
96
97
  return 'struct-bak';
97
- if (suffix.startsWith('bak-'))
98
+ if (/^bak-[A-Za-z0-9_.-]+$/.test(suffix))
98
99
  return 'bak';
99
- return 'misc';
100
+ if (/^codex-app-ui-repair-[A-Za-z0-9_.-]+\.bak$/.test(suffix))
101
+ return 'codex-app-ui-repair';
102
+ return null;
100
103
  }
101
104
  function messageOf(err) {
102
105
  return err instanceof Error ? err.message : String(err);