sneakoscope 4.6.4 → 4.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.
Files changed (44) 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 +73 -6
  7. package/dist/commands/codex-app.js +34 -3
  8. package/dist/commands/doctor.js +6 -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/glm-model-profile.js +1 -1
  16. package/dist/core/codex-app/glm-profile-installer.js +111 -7
  17. package/dist/core/codex-app/glm-profile-schema.js +21 -2
  18. package/dist/core/codex-control/codex-sdk-config-policy.js +2 -1
  19. package/dist/core/codex-model-guard.js +3 -1
  20. package/dist/core/commands/fast-mode-command.js +5 -2
  21. package/dist/core/commands/glm-command.js +2 -1
  22. package/dist/core/commands/mad-sks-command.js +24 -0
  23. package/dist/core/doctor/doctor-codex-startup-repair.js +111 -9
  24. package/dist/core/feature-registry.js +3 -3
  25. package/dist/core/fsx.js +1 -1
  26. package/dist/core/harness-guard.js +89 -3
  27. package/dist/core/init.js +9 -9
  28. package/dist/core/managed-assets/managed-assets-manifest.js +9 -8
  29. package/dist/core/pipeline-internals/runtime-core.js +3 -3
  30. package/dist/core/providers/glm/glm-52-profile.js +46 -0
  31. package/dist/core/providers/glm/glm-52-request.js +11 -2
  32. package/dist/core/providers/glm/glm-direct-run.js +1 -0
  33. package/dist/core/providers/glm/glm-mad-launch.js +3 -2
  34. package/dist/core/providers/glm/glm-profile-resolver.js +69 -4
  35. package/dist/core/providers/glm/naruto/glm-naruto-command.js +2 -1
  36. package/dist/core/team-live.js +2 -2
  37. package/dist/core/version.js +1 -1
  38. package/dist/scripts/agent-role-config-repair-check.js +5 -3
  39. package/dist/scripts/codex-native-reference-cache-check.js +8 -1
  40. package/dist/scripts/doctor-codex-startup-repair-check.js +21 -0
  41. package/dist/scripts/naruto-shadow-clone-swarm-check.js +11 -3
  42. package/dist/scripts/release-readiness-report.js +1 -1
  43. package/dist/scripts/sks-3-1-6-directive-check-lib.js +11 -0
  44. package/package.json +1 -1
@@ -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
  }
@@ -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
@@ -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) {
@@ -154,7 +154,7 @@ export function buildAllFeaturesSelftest(registry, opts = {}) {
154
154
  checkRow('native_agent_intake_contract_present', registry.features.some((feature) => feature.id === 'route-native-agent-intake'), ['route-native-agent-intake']),
155
155
  checkRow('cli_agent_fixture_pass', registry.features.some((feature) => feature.id === 'cli-agent' && feature.fixture?.status === 'pass' && feature.fixture.expected_artifacts?.some((artifact) => expectedArtifactPath(artifact).includes('agent-proof-evidence'))), ['cli-agent']),
156
156
  checkRow('agent_proof_evidence_contract_present', registry.features.some((feature) => feature.id === 'proof-agent-evidence'), ['proof-agent-evidence']),
157
- checkRow('agent_lease_policy_present', registry.features.some((feature) => feature.id === 'route-native-agent-intake' && /read-only/i.test(JSON.stringify(feature.contract || {})) && /lease/i.test(JSON.stringify(feature.contract || {}))), ['route-native-agent-intake']),
157
+ checkRow('agent_lease_policy_present', registry.features.some((feature) => feature.id === 'route-native-agent-intake' && /bounded workspace-write/i.test(JSON.stringify(feature.contract || {})) && /lease/i.test(JSON.stringify(feature.contract || {}))), ['route-native-agent-intake']),
158
158
  checkRow('fixture_pass_threshold', (fixturesSummary.counts.pass || 0) >= 90, [`pass=${fixturesSummary.counts.pass || 0}`]),
159
159
  checkRow('fixture_not_required_ceiling', (fixturesSummary.counts.not_required || 0) <= 16, [`not_required=${fixturesSummary.counts.not_required || 0}`]),
160
160
  checkRow('fixture_mock_blocked_zero', (fixturesSummary.counts.blocked || 0) === 0, [`blocked=${fixturesSummary.counts.blocked || 0}`]),
@@ -628,7 +628,7 @@ function nativeAgentIntakeFeature() {
628
628
  aliases: ['sks team "task" [executor:5 reviewer:6 user:1]'],
629
629
  category: 'proof-route',
630
630
  maturity: 'stable',
631
- intent: 'Default read-only native multi-session agent intake before serious route implementation.',
631
+ intent: 'Default bounded workspace-write native multi-session agent intake before serious route implementation.',
632
632
  voxel_triwiki_integration: 'native agent findings are TriWiki-ready and can require image voxel evidence for visual routes',
633
633
  completion_proof_integration: 'Completion Proof evidence.agents records agent_count, route, leases, no-overlap proof, cleanup, proof graph, and dynamic effort policy',
634
634
  known_gaps: ['real speedup claims require runtime timing/eval evidence; mock/static timing is not enough'],
@@ -636,7 +636,7 @@ function nativeAgentIntakeFeature() {
636
636
  input: 'serious route mission, route collaboration fixture, or explicit sks agent run',
637
637
  output: 'agents/agent-central-ledger.json, agents/agent-task-board.json, agents/agent-leases.json, agents/agent-no-overlap-proof.json, agents/agent-session-cleanup.json, agents/agent-proof-evidence.json, agents/agent-effort-policy.json',
638
638
  state: 'mission-local native agent artifacts',
639
- safety: 'read-only analysis agents; central leases prevent overlapping write scopes; parent owns integration',
639
+ safety: 'bounded workspace-write analysis agents; central leases prevent overlapping write scopes; parent owns integration',
640
640
  proof: 'evidence.agents required for serious native route proof',
641
641
  voxel: 'visual agent records image voxel requirements without satisfying visual evidence by itself',
642
642
  tests: 'unit, integration, e2e route fixtures, native release gate scripts',
package/dist/core/fsx.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- export const PACKAGE_VERSION = '4.6.4';
8
+ export const PACKAGE_VERSION = '4.7.0';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
@@ -125,8 +125,9 @@ export function classifyHarnessPayload(root, payload = {}, policy = {}) {
125
125
  const command = extractCommand(payload);
126
126
  const writeIntent = hasWriteIntent(toolName, command, hay);
127
127
  const maintenance = classifyMaintenanceCommand(command || hay);
128
- const protectedMatches = findProtectedMatches(root, strings, policy);
129
- const packageEdit = writeIntent && /\b(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)\b/i.test(hay) && /\bsneakoscope\b/i.test(hay);
128
+ const writeTargets = extractWriteTargets(root, payload, strings, command);
129
+ const protectedMatches = findProtectedMatches(root, writeTargets.length ? writeTargets : strings, policy);
130
+ const packageEdit = writeIntent && packageManifestEditDetected(writeTargets, hay);
130
131
  const block = maintenance.block || packageEdit || (writeIntent && protectedMatches.length > 0);
131
132
  const reasons = [];
132
133
  if (maintenance.block)
@@ -135,7 +136,7 @@ export function classifyHarnessPayload(root, payload = {}, policy = {}) {
135
136
  reasons.push('package_manifest_sneakoscope_edit_blocked');
136
137
  if (writeIntent && protectedMatches.length)
137
138
  reasons.push('protected_harness_path_write_blocked');
138
- return { block, reasons: [...new Set(reasons)], matches: protectedMatches, writeIntent, toolName, command };
139
+ return { block, reasons: [...new Set(reasons)], matches: protectedMatches, writeIntent, toolName, command, writeTargets };
139
140
  }
140
141
  export async function collectHarnessFingerprints(root) {
141
142
  const out = {};
@@ -196,6 +197,91 @@ function collectPayloadStrings(obj, out = [], depth = 0) {
196
197
  }
197
198
  return out;
198
199
  }
200
+ function extractWriteTargets(root, payload = {}, strings = [], command = '') {
201
+ const targets = new Set();
202
+ for (const value of collectPathFieldStrings(payload))
203
+ addWriteTarget(root, targets, value);
204
+ for (const text of [command, ...strings]) {
205
+ const s = String(text || '');
206
+ for (const match of s.matchAll(/^\*\*\*\s+(?:Update|Add|Delete)\s+File:\s+(.+)$/gmi))
207
+ addWriteTarget(root, targets, match[1]);
208
+ for (const match of s.matchAll(/^\*\*\*\s+Move to:\s+(.+)$/gmi))
209
+ addWriteTarget(root, targets, match[1]);
210
+ for (const match of s.matchAll(/>{1,2}\s*(['"]?)([^\s;&|'"`]+)\1/g))
211
+ addWriteTarget(root, targets, match[2]);
212
+ collectShellCommandTargets(root, targets, s);
213
+ }
214
+ return [...targets].sort();
215
+ }
216
+ function collectPathFieldStrings(obj, out = [], depth = 0) {
217
+ if (depth > 8 || obj == null)
218
+ return out;
219
+ if (Array.isArray(obj)) {
220
+ for (const value of obj)
221
+ collectPathFieldStrings(value, out, depth + 1);
222
+ return out;
223
+ }
224
+ if (typeof obj !== 'object')
225
+ return out;
226
+ for (const [key, value] of Object.entries(obj)) {
227
+ const normalized = String(key || '').toLowerCase();
228
+ if (typeof value === 'string' && /^(?:path|file|filename|target|destination|dest|to|from|cwd|workdir|file_path|target_path|output_path|artifact_path)$/.test(normalized))
229
+ out.push(value);
230
+ collectPathFieldStrings(value, out, depth + 1);
231
+ }
232
+ return out;
233
+ }
234
+ function collectShellCommandTargets(root, targets, text) {
235
+ const words = shellWords(text);
236
+ for (let i = 0; i < words.length; i += 1) {
237
+ const word = words[i] || '';
238
+ if (!/^(?:rm|rmdir|mv|cp|touch|mkdir|chmod|chown)$/.test(word))
239
+ continue;
240
+ for (let j = i + 1; j < words.length; j += 1) {
241
+ const candidate = words[j] || '';
242
+ if (!candidate || candidate.startsWith('-'))
243
+ continue;
244
+ if (/^(?:&&|\|\||;|\|)$/.test(candidate))
245
+ break;
246
+ if (/^(?:rm|rmdir|mv|cp|touch|mkdir|chmod|chown|git|npm|node|python\d?)$/.test(candidate) && j > i + 1)
247
+ break;
248
+ addWriteTarget(root, targets, candidate);
249
+ }
250
+ }
251
+ }
252
+ function shellWords(text) {
253
+ return [...String(text || '').matchAll(/"([^"]*)"|'([^']*)'|([^\s]+)/g)].map((match) => match[1] || match[2] || match[3]).filter(Boolean);
254
+ }
255
+ function addWriteTarget(root, targets, value) {
256
+ const normalized = normalizeTargetPath(root, value);
257
+ if (normalized)
258
+ targets.add(normalized);
259
+ }
260
+ function normalizeTargetPath(root, value) {
261
+ let s = String(value || '').trim();
262
+ if (!s)
263
+ return '';
264
+ s = s.replace(/^['"`<]+|['"`>,]+$/g, '');
265
+ if (!s || /^\$[\w{]/.test(s) || /^[A-Z_][A-Z0-9_]*=/.test(s))
266
+ return '';
267
+ if (/^(?:--?|&&|\|\||;|\|)$/.test(s))
268
+ return '';
269
+ s = s.replace(/\\/g, '/');
270
+ if (s.startsWith('a/') || s.startsWith('b/'))
271
+ s = s.slice(2);
272
+ if (s.startsWith('./'))
273
+ s = s.slice(2);
274
+ const rootNorm = String(root || '').replace(/\\/g, '/').replace(/\/$/, '');
275
+ if (rootNorm && s.startsWith(`${rootNorm}/`))
276
+ s = s.slice(rootNorm.length + 1);
277
+ return s.replace(/\/$/, '');
278
+ }
279
+ function packageManifestEditDetected(writeTargets, hay) {
280
+ const hasPackageTarget = writeTargets.length > 0
281
+ ? writeTargets.some((target) => /(^|\/)(?:package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$/.test(target))
282
+ : /\b(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)\b/i.test(hay);
283
+ return hasPackageTarget && /\bsneakoscope\b/i.test(hay);
284
+ }
199
285
  function hasWriteIntent(toolName, command, hay) {
200
286
  if (/\b(apply_patch|edit|write|create|delete|remove|rename|str_replace|file_write|fs_write)\b/i.test(toolName))
201
287
  return true;