sneakoscope 4.6.5 → 4.7.1

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 (48) hide show
  1. package/README.md +18 -4
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/cli/install-helpers.js +80 -15
  7. package/dist/commands/codex-app.js +34 -3
  8. package/dist/commands/doctor.js +1 -1
  9. package/dist/core/agents/agent-effort-policy.js +110 -5
  10. package/dist/core/agents/agent-orchestrator.js +27 -3
  11. package/dist/core/agents/agent-plan.js +2 -2
  12. package/dist/core/agents/agent-roster.js +8 -0
  13. package/dist/core/agents/agent-worker-slot.js +4 -0
  14. package/dist/core/codex/agent-config-file-repair.js +4 -4
  15. package/dist/core/codex-app/codex-app-fast-ui-repair.js +4 -2
  16. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +14 -3
  17. package/dist/core/codex-app/glm-model-profile.js +1 -1
  18. package/dist/core/codex-app/glm-profile-installer.js +111 -7
  19. package/dist/core/codex-app/glm-profile-schema.js +21 -2
  20. package/dist/core/codex-app.js +2 -0
  21. package/dist/core/codex-control/codex-sdk-config-policy.js +2 -1
  22. package/dist/core/codex-model-guard.js +3 -1
  23. package/dist/core/commands/fast-mode-command.js +5 -2
  24. package/dist/core/commands/glm-command.js +2 -1
  25. package/dist/core/commands/mad-sks-command.js +24 -0
  26. package/dist/core/doctor/doctor-codex-startup-repair.js +111 -9
  27. package/dist/core/feature-registry.js +3 -3
  28. package/dist/core/fsx.js +1 -1
  29. package/dist/core/harness-guard.js +89 -3
  30. package/dist/core/init.js +13 -16
  31. package/dist/core/managed-assets/managed-assets-manifest.js +9 -8
  32. package/dist/core/pipeline-internals/runtime-core.js +3 -3
  33. package/dist/core/providers/glm/glm-52-profile.js +46 -0
  34. package/dist/core/providers/glm/glm-52-request.js +11 -2
  35. package/dist/core/providers/glm/glm-direct-run.js +1 -0
  36. package/dist/core/providers/glm/glm-mad-launch.js +3 -2
  37. package/dist/core/providers/glm/glm-profile-resolver.js +69 -4
  38. package/dist/core/providers/glm/naruto/glm-naruto-command.js +2 -1
  39. package/dist/core/team-live.js +2 -2
  40. package/dist/core/version.js +1 -1
  41. package/dist/scripts/agent-role-config-repair-check.js +5 -3
  42. package/dist/scripts/codex-native-reference-cache-check.js +8 -1
  43. package/dist/scripts/doctor-codex-startup-repair-check.js +21 -0
  44. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +6 -2
  45. package/dist/scripts/naruto-shadow-clone-swarm-check.js +11 -3
  46. package/dist/scripts/release-readiness-report.js +1 -1
  47. package/dist/scripts/sks-3-1-6-directive-check-lib.js +11 -0
  48. package/package.json +1 -1
@@ -112,8 +112,12 @@ export function buildAgentRoster(opts = {}) {
112
112
  index: index + 1,
113
113
  write_policy: persona.write_policy,
114
114
  status: 'pending',
115
+ model: effort.model,
115
116
  reasoning_effort: effort.reasoning_effort,
116
117
  model_reasoning_effort: effort.model_reasoning_effort,
118
+ model_tier: effort.model_tier,
119
+ model_profile: effort.model_profile,
120
+ model_selection_reason: effort.model_selection_reason,
117
121
  reasoning_profile: effort.reasoning_profile,
118
122
  service_tier: effort.service_tier,
119
123
  reasoning_reason: effort.reason,
@@ -192,8 +196,12 @@ export function buildNarutoCloneRoster(opts = {}) {
192
196
  index: index + 1,
193
197
  write_policy: cloneReadonly ? 'read-only' : 'exclusive Naruto patch-envelope lease required',
194
198
  status: 'pending',
199
+ model: effort.model,
195
200
  reasoning_effort: effort.reasoning_effort,
196
201
  model_reasoning_effort: effort.model_reasoning_effort,
202
+ model_tier: effort.model_tier,
203
+ model_profile: effort.model_profile,
204
+ model_selection_reason: effort.model_selection_reason,
197
205
  reasoning_profile: effort.reasoning_profile,
198
206
  service_tier: 'fast',
199
207
  fast_mode: true,
@@ -17,7 +17,11 @@ export function createAgentWorkerSlots(roster, targetActiveSlots) {
17
17
  persona_id: String(persona.persona_id || persona.id || `agent_${index + 1}`),
18
18
  role: String(persona.role || 'verifier'),
19
19
  write_policy: String(persona.write_policy || 'read-only'),
20
+ model: persona.model || null,
20
21
  reasoning_effort: persona.reasoning_effort || persona.model_reasoning_effort || null,
22
+ model_reasoning_effort: persona.model_reasoning_effort || null,
23
+ model_tier: persona.model_tier || null,
24
+ model_profile: persona.model_profile || null,
21
25
  reasoning_profile: persona.reasoning_profile || null,
22
26
  service_tier: persona.service_tier || 'fast',
23
27
  fast_mode: persona.fast_mode !== false
@@ -175,15 +175,15 @@ function minimalManagedConfigToml() {
175
175
  '[mcp_servers.context7]',
176
176
  'url = "https://mcp.context7.com/mcp"',
177
177
  '',
178
- agentConfigBlock('native_agent', 'Read-only SKS analysis agent.', './agents/native-agent-intake.toml', ['Analysis', 'Mapper']),
178
+ agentConfigBlock('native_agent', 'SKS native agent with bounded write capability.', './agents/native-agent-intake.toml', ['Analysis', 'Mapper']),
179
179
  '',
180
- agentConfigBlock('team_consensus', 'SKS planning/debate agent.', './agents/team-consensus.toml', ['Consensus', 'Atlas']),
180
+ agentConfigBlock('team_consensus', 'SKS planning/debate agent with bounded write capability.', './agents/team-consensus.toml', ['Consensus', 'Atlas']),
181
181
  '',
182
182
  agentConfigBlock('implementation_worker', 'SKS bounded implementation worker.', './agents/implementation-worker.toml', ['Builder', 'Mason']),
183
183
  '',
184
- agentConfigBlock('db_safety_reviewer', 'Read-only DB safety reviewer.', './agents/db-safety-reviewer.toml', ['Sentinel', 'Ledger']),
184
+ agentConfigBlock('db_safety_reviewer', 'DB safety reviewer with bounded write capability.', './agents/db-safety-reviewer.toml', ['Sentinel', 'Ledger']),
185
185
  '',
186
- agentConfigBlock('qa_reviewer', 'Read-only QA reviewer.', './agents/qa-reviewer.toml', ['Verifier', 'Reviewer']),
186
+ agentConfigBlock('qa_reviewer', 'QA reviewer with bounded write capability.', './agents/qa-reviewer.toml', ['Verifier', 'Reviewer']),
187
187
  ''
188
188
  ].join('\n');
189
189
  }
@@ -4,7 +4,8 @@ import { ensureDir, nowIso, readText, sha256, writeJsonAtomic, writeTextAtomic }
4
4
  import { codexHome, scanProjectLocalForbiddenKeys, snapshotCodexAppUiState } from './codex-app-ui-state-snapshot.js';
5
5
  import { assertCodexAppUiMutationAllowed } from './codex-app-ui-clobber-guard.js';
6
6
  export const CODEX_APP_FAST_UI_REPAIR_SCHEMA = 'sks.codex-app-fast-ui-repair.v1';
7
- const FAST_UI_TOP_LEVEL_RE = /^\s*service_tier\s*=/;
7
+ const FAST_UI_TOP_LEVEL_RE = /^\s*(?:model|model_reasoning_effort|service_tier)\s*=/;
8
+ const CODEX_APP_MODE_LOCK_TOP_LEVEL_RE = /^\s*(?:model|model_reasoning_effort)\s*=/;
8
9
  const FAST_UI_FEATURE_LINE_RE = /^\s*fast_mode\s*=/;
9
10
  const FAST_UI_USER_TABLE_LINE_RE = /^\s*(enabled|visible|locked|hidden|disabled)\s*=/;
10
11
  const SKS_CAUSED_RE = /(?:SKS|Sneakoscope|codex-lb|sks-mad|sks fast)/i;
@@ -125,10 +126,11 @@ function stripProjectLocalForbiddenKeys(text) {
125
126
  }
126
127
  function stripSksCausedHostOwnedLines(text) {
127
128
  return stripMatchingLines(text, (line, table, previous, next) => {
129
+ const isCodexAppModeLock = !table && CODEX_APP_MODE_LOCK_TOP_LEVEL_RE.test(line);
128
130
  const isFastUiLine = FAST_UI_TOP_LEVEL_RE.test(line)
129
131
  || (table === 'features' && FAST_UI_FEATURE_LINE_RE.test(line))
130
132
  || (table === 'user.fast_mode' && FAST_UI_USER_TABLE_LINE_RE.test(line));
131
- return isFastUiLine && (SKS_CAUSED_RE.test(line) || SKS_CAUSED_RE.test(previous) || SKS_CAUSED_RE.test(next));
133
+ return isCodexAppModeLock || (isFastUiLine && (SKS_CAUSED_RE.test(line) || SKS_CAUSED_RE.test(previous) || SKS_CAUSED_RE.test(next)));
132
134
  });
133
135
  }
134
136
  function stripMatchingLines(text, shouldRemove) {
@@ -4,6 +4,8 @@ import path from 'node:path';
4
4
  import { exists, nowIso, readText, sha256, writeJsonAtomic } from '../fsx.js';
5
5
  export const CODEX_APP_UI_STATE_SNAPSHOT_SCHEMA = 'sks.codex-app-ui-state-snapshot.v1';
6
6
  export const PROJECT_LOCAL_FORBIDDEN_CODEX_KEYS = [
7
+ 'model',
8
+ 'model_reasoning_effort',
7
9
  'openai_base_url',
8
10
  'chatgpt_base_url',
9
11
  'apps_mcp_product_sku',
@@ -15,7 +17,7 @@ export const PROJECT_LOCAL_FORBIDDEN_CODEX_KEYS = [
15
17
  'experimental_realtime_ws_base_url',
16
18
  'otel'
17
19
  ];
18
- const HOST_OWNED_KEY_RE = /^(?:openai_base_url|chatgpt_base_url|apps_mcp_product_sku|model_provider|model_providers(?:\.|$)|notify|profile|profiles(?:\.|$)|experimental_realtime_ws_base_url|otel(?:\.|$)|features\.fast_mode|service_tier|user\.fast_mode(?:\.|$))/;
20
+ const HOST_OWNED_KEY_RE = /^(?:model|model_reasoning_effort|openai_base_url|chatgpt_base_url|apps_mcp_product_sku|model_provider|model_providers(?:\.|$)|notify|profile|profiles(?:\.|$)|experimental_realtime_ws_base_url|otel(?:\.|$)|features\.fast_mode|service_tier|user\.fast_mode(?:\.|$))/;
19
21
  const SECRET_KEY_RE = /(?:key|token|secret|password|credential|cookie|authorization|bearer|refresh|access)/i;
20
22
  export function codexHome(input) {
21
23
  return path.resolve(String(input?.codexHome || process.env.CODEX_HOME || path.join(os.homedir(), '.codex')));
@@ -35,11 +37,16 @@ export async function snapshotCodexAppUiState(root = process.cwd(), input = {})
35
37
  const providerSignals = hostOwnedSignals
36
38
  .filter((signal) => signal.provider_related)
37
39
  .map((signal) => `${signal.key_path}=${signal.value_preview}`);
38
- const fastSelectorLocked = hostOwnedSignals.some((signal) => {
40
+ const baseConfigHostOwnedSignals = files
41
+ .filter((file) => !isProfileConfigSnapshotPath(file.path))
42
+ .flatMap((file) => file.signals.filter((signal) => signal.host_owned));
43
+ const fastSelectorLocked = baseConfigHostOwnedSignals.some((signal) => {
39
44
  if (signal.key_path === 'features.fast_mode' && signal.value_preview === 'false')
40
45
  return true;
41
46
  if (signal.key_path.startsWith('user.fast_mode') && /hidden|fixed|disabled|false/i.test(signal.value_preview))
42
47
  return true;
48
+ if (signal.key_path === 'model' || signal.key_path === 'model_reasoning_effort')
49
+ return true;
43
50
  if (signal.key_path === 'service_tier' && signal.sks_related)
44
51
  return true;
45
52
  return false;
@@ -120,7 +127,7 @@ export function scanTomlSignals(text) {
120
127
  value_hash: sha256(value),
121
128
  line: index + 1,
122
129
  host_owned: HOST_OWNED_KEY_RE.test(keyPath),
123
- fast_ui_related: /(?:fast_mode|service_tier)/i.test(keyPath) || /(?:fast|priority|default)/i.test(value),
130
+ fast_ui_related: /(?:fast_mode|service_tier|model_reasoning_effort|^model$)/i.test(keyPath) || /(?:fast|priority|default)/i.test(value),
124
131
  provider_related: /(?:provider|base_url|auth|profile|openai|chatgpt|codex-lb)/i.test(lowerPath),
125
132
  sks_related: /(?:sks|sneakoscope|codex-lb)/i.test(lineText)
126
133
  });
@@ -176,6 +183,10 @@ async function profileConfigFiles(home) {
176
183
  return [];
177
184
  }
178
185
  }
186
+ function isProfileConfigSnapshotPath(file) {
187
+ const base = path.basename(file);
188
+ return /^profile-.+\.config\.toml$/.test(base) || /^[A-Za-z0-9_-]+\.config\.toml$/.test(base);
189
+ }
179
190
  async function readAuthMetadata(file) {
180
191
  const text = await readText(file, null);
181
192
  if (text == null)
@@ -1,2 +1,2 @@
1
- export { GLM_CODEX_APP_PROFILE_ID, GLM_CODEX_APP_PROFILE_LABEL, buildGlmCodexAppModelProfile } from '../providers/glm/glm-52-profile.js';
1
+ export { GLM_CODEX_APP_PROFILE_ID, GLM_CODEX_APP_PROFILE_LABEL, GLM_CODEX_CONFIG_PROFILE_ID, GLM_CODEX_CONFIG_PROVIDER_ID, GLM_CODEX_CONFIG_REASONING_PROFILES, GLM_CODEX_SELECTABLE_REASONING_EFFORTS, buildGlmCodexAppModelProfile } from '../providers/glm/glm-52-profile.js';
2
2
  //# sourceMappingURL=glm-model-profile.js.map
@@ -1,30 +1,49 @@
1
1
  import path from 'node:path';
2
- import { readJson, writeJsonAtomic, nowIso } from '../fsx.js';
3
- import { buildGlmCodexAppModelProfile } from './glm-model-profile.js';
2
+ import { codexLbConfigPath, ensureGlobalCodexAppGlmProfile } from '../../cli/install-helpers.js';
3
+ import { readJson, readText, writeJsonAtomic, nowIso } from '../fsx.js';
4
+ import { GLM_CODEX_CONFIG_PROFILE_ID, GLM_CODEX_CONFIG_PROVIDER_ID, GLM_CODEX_CONFIG_REASONING_PROFILES, buildGlmCodexAppModelProfile } from './glm-model-profile.js';
4
5
  import { validateGlmCodexAppModelProfile } from './glm-profile-schema.js';
5
6
  import { resolveOpenRouterApiKey } from '../providers/openrouter/openrouter-secret-store.js';
7
+ import { GLM_52_OPENROUTER_MODEL } from '../providers/glm/glm-52-settings.js';
6
8
  export async function installCodexAppGlmProfile(input) {
7
9
  const root = path.resolve(input.root);
8
10
  const profile = buildGlmCodexAppModelProfile();
9
11
  const profilePath = path.join(root, '.sneakoscope', 'codex-app', 'glm-model-profile.json');
10
12
  const reportPath = path.join(root, '.sneakoscope', 'reports', 'codex-app-glm-profile.json');
11
13
  const key = await resolveOpenRouterApiKey({ env: input.env || process.env });
14
+ const home = input.home || input.env?.HOME;
15
+ const configWrite = input.apply === false
16
+ ? null
17
+ : await ensureGlobalCodexAppGlmProfile({ home, configPath: input.configPath });
18
+ const configStatus = input.apply === false
19
+ ? await previewCodexAppGlmConfigStatus({ home, configPath: input.configPath })
20
+ : await inspectCodexAppGlmConfig({ home, configPath: input.configPath });
21
+ const configWriteBlockers = configWrite?.ok === false
22
+ ? [`glm_codex_app_config_${configWrite.status || 'failed'}`]
23
+ : [];
24
+ const blockers = input.apply === false ? [] : [...configWriteBlockers, ...configStatus.blockers];
12
25
  const warnings = [
13
26
  ...key.warnings,
14
- ...(key.key ? [] : ['openrouter_key_missing_until_sks_--mad_--glm_--repair'])
27
+ ...(key.key ? [] : ['openrouter_key_missing_until_sks_--mad_--glm_--repair']),
28
+ ...(input.apply === false ? ['codex_desktop_glm_config_not_written_apply_false'] : [])
15
29
  ];
16
30
  if (input.apply !== false)
17
31
  await writeJsonAtomic(profilePath, profile);
18
32
  const result = {
19
33
  schema: 'sks.codex-app-glm-profile-result.v1',
20
34
  generated_at: nowIso(),
21
- ok: true,
22
- status: input.apply === false ? 'valid' : 'installed',
35
+ ok: blockers.length === 0,
36
+ status: blockers.length === 0 ? input.apply === false ? 'valid' : 'installed' : 'blocked',
23
37
  profile,
24
38
  profile_path: '.sneakoscope/codex-app/glm-model-profile.json',
25
39
  report_path: '.sneakoscope/reports/codex-app-glm-profile.json',
40
+ config_path: configStatus.config_path,
41
+ codex_config_profile: GLM_CODEX_CONFIG_PROFILE_ID,
42
+ codex_reasoning_profiles: GLM_CODEX_CONFIG_REASONING_PROFILES.map((item) => item.id),
43
+ config_write: configWrite,
44
+ config_status: configStatus,
26
45
  openrouter_key_source: key.source,
27
- blockers: [],
46
+ blockers,
28
47
  warnings
29
48
  };
30
49
  await writeJsonAtomic(reportPath, result).catch(() => undefined);
@@ -38,7 +57,9 @@ export async function doctorCodexAppGlmProfile(input) {
38
57
  const validation = validateGlmCodexAppModelProfile(existing);
39
58
  const key = await resolveOpenRouterApiKey({ env: input.env || process.env });
40
59
  const profile = validation.profile || buildGlmCodexAppModelProfile();
41
- const blockers = [...validation.blockers];
60
+ const home = input.home || input.env?.HOME;
61
+ const configStatus = await inspectCodexAppGlmConfig({ home, configPath: input.configPath });
62
+ const blockers = [...validation.blockers, ...configStatus.blockers];
42
63
  const warnings = [
43
64
  ...key.warnings,
44
65
  ...(key.key ? [] : ['openrouter_key_missing_until_sks_--mad_--glm_--repair'])
@@ -51,6 +72,11 @@ export async function doctorCodexAppGlmProfile(input) {
51
72
  profile,
52
73
  profile_path: '.sneakoscope/codex-app/glm-model-profile.json',
53
74
  report_path: '.sneakoscope/reports/codex-app-glm-profile.json',
75
+ config_path: configStatus.config_path,
76
+ codex_config_profile: GLM_CODEX_CONFIG_PROFILE_ID,
77
+ codex_reasoning_profiles: GLM_CODEX_CONFIG_REASONING_PROFILES.map((item) => item.id),
78
+ config_write: null,
79
+ config_status: configStatus,
54
80
  openrouter_key_source: key.source,
55
81
  blockers,
56
82
  warnings
@@ -58,4 +84,82 @@ export async function doctorCodexAppGlmProfile(input) {
58
84
  await writeJsonAtomic(reportPath, result).catch(() => undefined);
59
85
  return result;
60
86
  }
87
+ async function previewCodexAppGlmConfigStatus(input) {
88
+ const configPath = input.configPath || codexLbConfigPath(input.home);
89
+ return {
90
+ schema: 'sks.codex-app-glm-config-status.v1',
91
+ ok: true,
92
+ config_path: configPath,
93
+ provider_present: false,
94
+ profiles_present: [],
95
+ profiles_missing: [],
96
+ blockers: []
97
+ };
98
+ }
99
+ async function inspectCodexAppGlmConfig(input) {
100
+ const configPath = input.configPath || codexLbConfigPath(input.home);
101
+ const text = await readText(configPath, '').catch(() => '');
102
+ const providerBody = tomlTableBody(text, `model_providers.${GLM_CODEX_CONFIG_PROVIDER_ID}`);
103
+ const providerPresent = Boolean(providerBody);
104
+ const blockers = [];
105
+ if (!providerPresent) {
106
+ blockers.push('glm_codex_app_config_missing_openrouter_provider');
107
+ }
108
+ else {
109
+ if (!hasTomlString(providerBody, 'base_url', 'https://openrouter.ai/api/v1'))
110
+ blockers.push('glm_codex_app_config_invalid_openrouter_base_url');
111
+ if (!hasTomlString(providerBody, 'wire_api', 'responses'))
112
+ blockers.push('glm_codex_app_config_invalid_openrouter_wire_api');
113
+ if (!hasTomlString(providerBody, 'env_key', 'OPENROUTER_API_KEY'))
114
+ blockers.push('glm_codex_app_config_invalid_openrouter_env_key');
115
+ }
116
+ const profilesPresent = [];
117
+ const profilesMissing = [];
118
+ for (const profile of GLM_CODEX_CONFIG_REASONING_PROFILES) {
119
+ const body = tomlTableBody(text, `profiles.${profile.id}`);
120
+ if (!body) {
121
+ profilesMissing.push(profile.id);
122
+ blockers.push(`glm_codex_app_config_missing_profile:${profile.id}`);
123
+ continue;
124
+ }
125
+ profilesPresent.push(profile.id);
126
+ if (!hasTomlString(body, 'model_provider', GLM_CODEX_CONFIG_PROVIDER_ID))
127
+ blockers.push(`glm_codex_app_config_invalid_profile_provider:${profile.id}`);
128
+ if (!hasTomlString(body, 'model', GLM_52_OPENROUTER_MODEL))
129
+ blockers.push(`glm_codex_app_config_invalid_profile_model:${profile.id}`);
130
+ if (!hasTomlString(body, 'model_reasoning_effort', profile.reasoning_effort))
131
+ blockers.push(`glm_codex_app_config_invalid_profile_reasoning:${profile.id}`);
132
+ }
133
+ return {
134
+ schema: 'sks.codex-app-glm-config-status.v1',
135
+ ok: blockers.length === 0,
136
+ config_path: configPath,
137
+ provider_present: providerPresent,
138
+ profiles_present: profilesPresent,
139
+ profiles_missing: profilesMissing,
140
+ blockers
141
+ };
142
+ }
143
+ function tomlTableBody(text, table) {
144
+ const header = `[${table}]`;
145
+ const lines = String(text || '').split('\n');
146
+ const start = lines.findIndex((line) => line.trim() === header);
147
+ if (start === -1)
148
+ return '';
149
+ const body = [];
150
+ for (let index = start + 1; index < lines.length; index += 1) {
151
+ const line = lines[index];
152
+ if (/^\s*\[[^\]]+\]\s*$/.test(line || ''))
153
+ break;
154
+ body.push(line || '');
155
+ }
156
+ return body.join('\n');
157
+ }
158
+ function hasTomlString(text, key, value) {
159
+ const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"${escapeRegExp(value)}"\\s*(?:#.*)?$`, 'm');
160
+ return pattern.test(text);
161
+ }
162
+ function escapeRegExp(value) {
163
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
164
+ }
61
165
  //# sourceMappingURL=glm-profile-installer.js.map
@@ -1,4 +1,4 @@
1
- import { GLM_CODEX_APP_PROFILE_ID, buildGlmCodexAppModelProfile } from './glm-model-profile.js';
1
+ import { GLM_CODEX_APP_PROFILE_ID, GLM_CODEX_CONFIG_PROFILE_ID, GLM_CODEX_CONFIG_PROVIDER_ID, GLM_CODEX_CONFIG_REASONING_PROFILES, GLM_CODEX_SELECTABLE_REASONING_EFFORTS, buildGlmCodexAppModelProfile } from './glm-model-profile.js';
2
2
  import { GLM_52_OPENROUTER_MODEL, GLM_MAD_MODE } from '../providers/glm/glm-52-settings.js';
3
3
  export function validateGlmCodexAppModelProfile(value) {
4
4
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -11,12 +11,16 @@ export function validateGlmCodexAppModelProfile(value) {
11
11
  profile.id === GLM_CODEX_APP_PROFILE_ID ? null : 'glm_codex_app_profile_invalid_id',
12
12
  profile.provider === 'openrouter' ? null : 'glm_codex_app_profile_invalid_provider',
13
13
  profile.model === GLM_52_OPENROUTER_MODEL ? null : 'glm_codex_app_profile_invalid_model',
14
+ profile.codexConfigProvider === GLM_CODEX_CONFIG_PROVIDER_ID ? null : 'glm_codex_app_profile_invalid_codex_config_provider',
15
+ profile.codexConfigProfile === GLM_CODEX_CONFIG_PROFILE_ID ? null : 'glm_codex_app_profile_invalid_codex_config_profile',
16
+ hasExpectedReasoningEfforts(profile.supportedReasoningEfforts) ? null : 'glm_codex_app_profile_invalid_reasoning_efforts',
17
+ hasExpectedReasoningProfiles(profile.reasoningProfiles) ? null : 'glm_codex_app_profile_invalid_reasoning_profiles',
14
18
  profile.mode === GLM_MAD_MODE ? null : 'glm_codex_app_profile_invalid_mode',
15
19
  profile.strictModelLock === true ? null : 'glm_codex_app_profile_not_strict',
16
20
  profile.gptFallbackAllowed === false ? null : 'glm_codex_app_profile_allows_gpt_fallback',
17
21
  profile.defaultProfile === 'speed' ? null : 'glm_codex_app_profile_default_not_speed',
18
22
  profile.defaultSettings?.tool_choice === 'none' ? null : 'glm_codex_app_profile_default_tools_not_omitted',
19
- profile.defaultSettings?.provider_require_parameters === true ? null : 'glm_codex_app_profile_default_does_not_require_parameters',
23
+ profile.defaultSettings?.provider_require_parameters === false ? null : 'glm_codex_app_profile_default_requires_parameters',
20
24
  profile.defaultSettings?.provider_allow_fallbacks === false ? null : 'glm_codex_app_profile_allows_provider_fallback'
21
25
  ].filter((item) => Boolean(item));
22
26
  return {
@@ -25,4 +29,19 @@ export function validateGlmCodexAppModelProfile(value) {
25
29
  profile: blockers.length === 0 ? profile : null
26
30
  };
27
31
  }
32
+ function hasExpectedReasoningEfforts(value) {
33
+ if (!Array.isArray(value)) {
34
+ return false;
35
+ }
36
+ return GLM_CODEX_SELECTABLE_REASONING_EFFORTS.every((effort) => value.includes(effort));
37
+ }
38
+ function hasExpectedReasoningProfiles(value) {
39
+ if (!Array.isArray(value)) {
40
+ return false;
41
+ }
42
+ return GLM_CODEX_CONFIG_REASONING_PROFILES.every((expected) => value.some((item) => item &&
43
+ typeof item === 'object' &&
44
+ item.id === expected.id &&
45
+ item.reasoning_effort === expected.reasoning_effort));
46
+ }
28
47
  //# sourceMappingURL=glm-profile-schema.js.map
@@ -798,6 +798,8 @@ async function codexFastModeConfigStatus(opts = {}) {
798
798
  if (!config.text)
799
799
  continue;
800
800
  const topLevel = topLevelToml(config.text);
801
+ if (/(^|\n)\s*model\s*=/.test(topLevel))
802
+ blockers.push(`${config.scope}:top_level_model`);
801
803
  if (/(^|\n)\s*model_reasoning_effort\s*=/.test(topLevel))
802
804
  blockers.push(`${config.scope}:top_level_model_reasoning_effort`);
803
805
  if (/(^|\n)\s*fast_default_opt_out\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(tomlTable(config.text, 'notice')))
@@ -1,6 +1,7 @@
1
1
  export function buildCodexSdkConfig(input) {
2
+ const model = String(input.model || process.env.SKS_CODEX_MODEL || process.env.CODEX_MODEL || 'gpt-5.5');
2
3
  const config = {
3
- model: String(process.env.SKS_CODEX_MODEL || process.env.CODEX_MODEL || 'gpt-5.5'),
4
+ model,
4
5
  service_tier: 'fast',
5
6
  model_reasoning_effort: String(input.modelReasoningEffort || input.reasoningEffort || process.env.SKS_CODEX_REASONING || process.env.CODEX_MODEL_REASONING_EFFORT || 'medium'),
6
7
  mcp_servers: {},
@@ -1,4 +1,6 @@
1
1
  export const REQUIRED_CODEX_MODEL = 'gpt-5.5';
2
+ export const GPT54_MINI_CODEX_MODEL = 'gpt-5.4-mini';
3
+ export const SUPPORTED_CODEX_MODELS = [REQUIRED_CODEX_MODEL, GPT54_MINI_CODEX_MODEL];
2
4
  const MODEL_VALUE_FLAGS = new Set(['--model', '-m']);
3
5
  const CONFIG_VALUE_FLAGS = new Set(['-c', '--config']);
4
6
  function isModelConfigOverride(value = '') {
@@ -43,6 +45,6 @@ export function forceGpt55CodexConfigArgs(args = []) {
43
45
  }
44
46
  export function isForbiddenCodexModel(value = '') {
45
47
  const model = String(value || '').trim().toLowerCase();
46
- return /^gpt-5\./.test(model) && model !== REQUIRED_CODEX_MODEL;
48
+ return /^gpt-5\./.test(model) && !SUPPORTED_CODEX_MODELS.includes(model);
47
49
  }
48
50
  //# sourceMappingURL=codex-model-guard.js.map
@@ -21,8 +21,11 @@ export async function fastModeCommand(args = []) {
21
21
  preference = null;
22
22
  }
23
23
  const policy = resolveFastModePolicy({ root });
24
- const codexFastModeRepair = action === 'on'
25
- ? await ensureGlobalCodexFastModeDuringInstall({ forceFastMode: true })
24
+ const codexFastModeRepair = action === 'on' || action === 'off'
25
+ ? await ensureGlobalCodexFastModeDuringInstall({
26
+ forceFastMode: action === 'on',
27
+ forceFastModeOff: action === 'off'
28
+ })
26
29
  : null;
27
30
  const result = {
28
31
  schema: FAST_MODE_COMMAND_SCHEMA,
@@ -5,6 +5,7 @@ import { runGlmDirectSpeedRun } from '../providers/glm/glm-direct-run.js';
5
5
  import { runGlmReadinessAndExit } from '../providers/glm/glm-readiness.js';
6
6
  import { runGlmInteractiveLaunch } from '../providers/glm/glm-interactive-launch.js';
7
7
  import { glmNarutoCommand } from '../providers/glm/naruto/glm-naruto-command.js';
8
+ import { stripGlmSlashModelArgs } from '../providers/glm/glm-profile-resolver.js';
8
9
  export async function glmCommand(args = []) {
9
10
  if (flag(args, '--naruto') || positionalArgs(args)[0] === 'naruto') {
10
11
  const narutoArgs = args.filter((a) => a !== '--naruto' && a !== 'naruto');
@@ -56,7 +57,7 @@ export async function glmCommand(args = []) {
56
57
  return result;
57
58
  }
58
59
  function extractGlmTask(args) {
59
- const positional = positionalArgs(args).map(String);
60
+ const positional = positionalArgs(stripGlmSlashModelArgs(args)).map(String);
60
61
  if (positional[0] === 'run')
61
62
  return positional.slice(1).join(' ').trim() || null;
62
63
  if (positional[0] === 'session')
@@ -332,6 +332,30 @@ function isMadGlmLaunch(args = [], deps = {}) {
332
332
  async function prepareMadGlmLaunchRuntime(madLaunch, deps = {}) {
333
333
  const keyResolution = await resolveMadGlmLaunchKey(process.env);
334
334
  const profile = buildMadGlmLaunchProfileNoWrite(deps?.glmArgs || []);
335
+ if (profile.blockers.length) {
336
+ const blocked = {
337
+ schema: 'sks.glm-mad-launch.v1',
338
+ ok: false,
339
+ status: 'blocked',
340
+ mission_id: madLaunch.mission_id,
341
+ provider: profile.provider,
342
+ model: profile.model,
343
+ glm_profile: profile.glm_profile,
344
+ glm_mode: profile.glm_mode,
345
+ model_reasoning_effort: profile.model_reasoning_effort,
346
+ gpt_fallback_allowed: false,
347
+ blockers: profile.blockers,
348
+ warnings: []
349
+ };
350
+ await writeJsonAtomic(path.join(madLaunch.dir, 'mad-glm-launch.json'), blocked);
351
+ await appendJsonlBounded(path.join(madLaunch.dir, 'events.jsonl'), {
352
+ ts: nowIso(),
353
+ type: 'mad_sks.glm_launch_blocked',
354
+ blockers: profile.blockers
355
+ });
356
+ console.error(`SKS GLM MAD launch blocked: ${profile.blockers.join(', ')}`);
357
+ return blocked;
358
+ }
335
359
  if (!keyResolution.key) {
336
360
  const blocked = {
337
361
  schema: 'sks.glm-mad-launch.v1',
@@ -5,12 +5,12 @@ import { REQUIRED_CODEX_MODEL } from '../codex-model-guard.js';
5
5
  import { ensureDir, exists, nowIso, readText, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
6
6
  export const DOCTOR_CODEX_STARTUP_REPAIR_SCHEMA = 'sks.doctor-codex-startup-repair.v1';
7
7
  const AGENT_ROLE_FILES = new Map([
8
- ['analysis_scout', { file: 'analysis-scout.toml', description: 'Read-only SKS scout.', sandbox: 'read-only', nicknames: ['Scout', 'Mapper'] }],
9
- ['native_agent', { file: 'native-agent-intake.toml', description: 'Read-only SKS analysis agent.', sandbox: 'read-only', nicknames: ['Analysis', 'Mapper'] }],
10
- ['team_consensus', { file: 'team-consensus.toml', description: 'SKS planning/debate agent.', sandbox: 'read-only', nicknames: ['Consensus', 'Atlas'] }],
8
+ ['analysis_scout', { file: 'analysis-scout.toml', description: 'SKS scout with bounded write capability.', sandbox: 'workspace-write', nicknames: ['Scout', 'Mapper'] }],
9
+ ['native_agent', { file: 'native-agent-intake.toml', description: 'SKS native agent with bounded write capability.', sandbox: 'workspace-write', nicknames: ['Analysis', 'Mapper'] }],
10
+ ['team_consensus', { file: 'team-consensus.toml', description: 'SKS planning/debate agent with bounded write capability.', sandbox: 'workspace-write', nicknames: ['Consensus', 'Atlas'] }],
11
11
  ['implementation_worker', { file: 'implementation-worker.toml', description: 'SKS bounded implementation worker.', sandbox: 'workspace-write', nicknames: ['Builder', 'Mason'] }],
12
- ['db_safety_reviewer', { file: 'db-safety-reviewer.toml', description: 'Read-only DB safety reviewer.', sandbox: 'read-only', nicknames: ['Sentinel', 'Ledger'] }],
13
- ['qa_reviewer', { file: 'qa-reviewer.toml', description: 'Read-only QA reviewer.', sandbox: 'read-only', nicknames: ['Verifier', 'Reviewer'] }]
12
+ ['db_safety_reviewer', { file: 'db-safety-reviewer.toml', description: 'DB safety reviewer with bounded write capability.', sandbox: 'workspace-write', nicknames: ['Sentinel', 'Ledger'] }],
13
+ ['qa_reviewer', { file: 'qa-reviewer.toml', description: 'QA reviewer with bounded write capability.', sandbox: 'workspace-write', nicknames: ['Verifier', 'Reviewer'] }]
14
14
  ]);
15
15
  const DIRECTIVE_ROLE_FILES = [
16
16
  'sks-explorer.toml',
@@ -42,7 +42,8 @@ export async function runDoctorCodexStartupRepair(input) {
42
42
  ...configs.flatMap((entry) => [
43
43
  ...entry.agent_config_files_repaired.map((file) => `${entry.scope} agent config_file now points at ${file}`),
44
44
  ...(entry.mcp_blocks_repaired || []).map((server) => `${entry.scope} MCP block repaired: ${server}`),
45
- ...entry.stale_mcp_blocks_removed.map((server) => `${entry.scope} stale MCP block removed: ${server}`)
45
+ ...entry.stale_mcp_blocks_removed.map((server) => `${entry.scope} stale MCP block removed: ${server}`),
46
+ ...entry.duplicate_toml_blocks_removed.map((header) => `${entry.scope} duplicate TOML table removed: ${header}`)
46
47
  ])
47
48
  ];
48
49
  const manualActions = [
@@ -84,7 +85,8 @@ async function inspectOrRepairConfig(candidate, fix, nodeReplCommandCandidates,
84
85
  mcp_blocks_repaired: [],
85
86
  optional_mcp_blocks_ignored: [],
86
87
  blockers: [],
87
- warnings: candidate.scope === 'global' ? ['codex_home_config_missing_optional'] : []
88
+ warnings: candidate.scope === 'global' ? ['codex_home_config_missing_optional'] : [],
89
+ duplicate_toml_blocks_removed: []
88
90
  };
89
91
  }
90
92
  let next = text;
@@ -92,13 +94,29 @@ async function inspectOrRepairConfig(candidate, fix, nodeReplCommandCandidates,
92
94
  const staleMcpBlocksRemoved = [];
93
95
  const mcpBlocksRepaired = [];
94
96
  const optionalMcpBlocksIgnored = [];
97
+ const duplicateTomlBlocksRemoved = [];
95
98
  const blockers = [];
96
99
  const warnings = [];
100
+ const duplicateRepair = inspectOrRepairDuplicateTomlBlocks(next, candidate, fix);
101
+ next = duplicateRepair.text;
102
+ duplicateTomlBlocksRemoved.push(...duplicateRepair.removed);
103
+ warnings.push(...duplicateRepair.warnings);
97
104
  for (const [tableName, role] of AGENT_ROLE_FILES) {
98
105
  const target = path.join(candidate.agentDir, role.file);
99
- const table = tomlBlock(next, `agents.${tableName}`);
106
+ let table = tomlBlock(next, `agents.${tableName}`);
100
107
  if (!table)
101
108
  continue;
109
+ const currentDescription = stringValue(table.text, 'description');
110
+ if (currentDescription !== role.description) {
111
+ if (!fix)
112
+ warnings.push(`agent_description_stale:${tableName}`);
113
+ else {
114
+ next = replaceOrInsertKey(next, table, 'description', `"${escapeToml(role.description)}"`);
115
+ table = tomlBlock(next, `agents.${tableName}`);
116
+ if (!table)
117
+ continue;
118
+ }
119
+ }
102
120
  const current = stringValue(table.text, 'config_file');
103
121
  const targetExists = await exists(target);
104
122
  const currentValid = Boolean(current && path.isAbsolute(current) && await exists(current));
@@ -146,9 +164,87 @@ async function inspectOrRepairConfig(candidate, fix, nodeReplCommandCandidates,
146
164
  mcp_blocks_repaired: mcpBlocksRepaired,
147
165
  optional_mcp_blocks_ignored: optionalMcpBlocksIgnored,
148
166
  blockers,
149
- warnings
167
+ warnings,
168
+ duplicate_toml_blocks_removed: duplicateTomlBlocksRemoved
169
+ };
170
+ }
171
+ function inspectOrRepairDuplicateTomlBlocks(text, candidate, fix) {
172
+ const blocks = tomlBlocks(text);
173
+ const groups = new Map();
174
+ for (const block of blocks) {
175
+ const list = groups.get(block.header) || [];
176
+ list.push(block);
177
+ groups.set(block.header, list);
178
+ }
179
+ const warnings = [];
180
+ const removals = [];
181
+ for (const [header, rows] of groups) {
182
+ if (rows.length < 2)
183
+ continue;
184
+ warnings.push(`duplicate_toml_table:${header}`);
185
+ const keep = selectDuplicateTomlBlockToKeep(header, rows, candidate);
186
+ for (let index = 0; index < rows.length; index += 1) {
187
+ if (index !== keep)
188
+ removals.push(rows[index]);
189
+ }
190
+ }
191
+ if (!removals.length || !fix)
192
+ return { text, warnings, removed: [] };
193
+ return {
194
+ text: removeBlocks(text, removals),
195
+ warnings,
196
+ removed: removals.map((block) => block.header)
150
197
  };
151
198
  }
199
+ function tomlBlocks(text) {
200
+ const source = String(text || '');
201
+ const matches = [...source.matchAll(/(^|\n)\s*\[([^\]]+)\]\s*(?:#.*)?(?:\n|$)/g)];
202
+ return matches.map((match, index) => {
203
+ const start = Number(match.index || 0) + (match[1] ? 1 : 0);
204
+ const next = matches[index + 1];
205
+ const end = next ? Number(next.index || 0) + (next[1] ? 1 : 0) : source.length;
206
+ return {
207
+ header: String(match[2] || '').trim(),
208
+ start,
209
+ end,
210
+ text: source.slice(start, end)
211
+ };
212
+ });
213
+ }
214
+ function selectDuplicateTomlBlockToKeep(header, rows, candidate) {
215
+ const agentName = header.startsWith('agents.') ? header.slice('agents.'.length) : '';
216
+ const role = agentName ? AGENT_ROLE_FILES.get(agentName) : undefined;
217
+ if (role) {
218
+ const target = path.join(candidate.agentDir, role.file);
219
+ return maxIndexBy(rows, (block, index) => {
220
+ const configFile = stringValue(block.text, 'config_file');
221
+ const description = stringValue(block.text, 'description');
222
+ return ((configFile === target ? 100 : 0) +
223
+ (configFile && path.isAbsolute(configFile) ? 20 : 0) +
224
+ (description === role.description ? 30 : 0) +
225
+ assignmentCount(block.text) -
226
+ index / 1000);
227
+ });
228
+ }
229
+ if (header.startsWith('mcp_servers.'))
230
+ return 0;
231
+ return maxIndexBy(rows, (block, index) => assignmentCount(block.text) - index / 1000);
232
+ }
233
+ function maxIndexBy(rows, score) {
234
+ let best = 0;
235
+ let bestScore = Number.NEGATIVE_INFINITY;
236
+ rows.forEach((row, index) => {
237
+ const value = score(row, index);
238
+ if (value > bestScore) {
239
+ best = index;
240
+ bestScore = value;
241
+ }
242
+ });
243
+ return best;
244
+ }
245
+ function assignmentCount(text) {
246
+ return String(text || '').split(/\r?\n/).filter((line) => /^\s*[A-Za-z0-9_.-]+\s*=/.test(line)).length;
247
+ }
152
248
  async function inspectOrRepairNodeRepl(text, fix, extraCandidates, includeDefaultCandidates) {
153
249
  const server = 'node_repl';
154
250
  const table = tomlBlock(text, `mcp_servers.${server}`);
@@ -223,6 +319,12 @@ async function repairAgentRoleFiles(root, codexHome) {
223
319
  await ensureDir(dir);
224
320
  await writeTextAtomic(file, roleConfigToml(name, role.description, role.sandbox));
225
321
  created.push(file);
322
+ continue;
323
+ }
324
+ if (!text.includes(`sandbox_mode = "${role.sandbox}"`) || text.includes('Do not edit files.')) {
325
+ await backupConfig(file, text, 'role-write-capable');
326
+ await writeTextAtomic(file, roleConfigToml(name, role.description, role.sandbox));
327
+ sanitized.push(file);
226
328
  }
227
329
  }
228
330
  for (const file of DIRECTIVE_ROLE_FILES) {