sneakoscope 4.1.0 → 4.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 (94) hide show
  1. package/README.md +16 -3
  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/command-registry.js +1 -1
  7. package/dist/cli/router.js +6 -1
  8. package/dist/commands/doctor.js +272 -127
  9. package/dist/core/auto-review.js +1 -1
  10. package/dist/core/codex/agent-config-file-repair.js +43 -2
  11. package/dist/core/codex-app/codex-agent-role-sync.js +4 -4
  12. package/dist/core/codex-control/codex-0142-capability.js +51 -6
  13. package/dist/core/codex-control/codex-app-server-v2-client.js +2 -2
  14. package/dist/core/codex-native/codex-native-feature-broker.js +50 -0
  15. package/dist/core/codex-native/native-capability-postcheck.js +59 -16
  16. package/dist/core/codex-native/native-capability-repair-matrix.js +77 -13
  17. package/dist/core/commands/mad-db-command.js +146 -51
  18. package/dist/core/commands/mad-sks-command.js +51 -61
  19. package/dist/core/db-safety.js +35 -37
  20. package/dist/core/doctor/doctor-dirty-planner.js +9 -4
  21. package/dist/core/doctor/doctor-native-capability-repair.js +42 -7
  22. package/dist/core/doctor/doctor-readiness-matrix.js +9 -5
  23. package/dist/core/doctor/doctor-repair-postcheck.js +10 -1
  24. package/dist/core/doctor/doctor-transaction.js +1 -1
  25. package/dist/core/doctor/supabase-mcp-repair.js +2 -2
  26. package/dist/core/feature-registry.js +1 -1
  27. package/dist/core/fsx.js +1 -1
  28. package/dist/core/init.js +5 -4
  29. package/dist/core/mad-db/mad-db-capability.js +203 -74
  30. package/dist/core/mad-db/mad-db-coordinator.js +287 -0
  31. package/dist/core/mad-db/mad-db-executor.js +156 -0
  32. package/dist/core/mad-db/mad-db-ledger.js +1 -1
  33. package/dist/core/mad-db/mad-db-lock.js +40 -0
  34. package/dist/core/mad-db/mad-db-operation-store.js +140 -0
  35. package/dist/core/mad-db/mad-db-policy-resolver.js +42 -22
  36. package/dist/core/mad-db/mad-db-policy.js +195 -0
  37. package/dist/core/mad-db/mad-db-postconditions.js +30 -0
  38. package/dist/core/mad-db/mad-db-recovery.js +27 -0
  39. package/dist/core/mad-db/mad-db-result-lifecycle.js +31 -102
  40. package/dist/core/mad-db/mad-db-runtime-profile.js +121 -0
  41. package/dist/core/mad-db/mad-db-target.js +64 -0
  42. package/dist/core/managed-assets/managed-assets-manifest.js +14 -4
  43. package/dist/core/pipeline-internals/runtime-core.js +40 -0
  44. package/dist/core/providers/glm/bench/glm-benchmark-runner.js +4 -3
  45. package/dist/core/providers/glm/bench/glm-benchmark-types.js +1 -1
  46. package/dist/core/release/release-gate-dag.js +6 -5
  47. package/dist/core/routes.js +23 -8
  48. package/dist/core/update/update-migration-state.js +265 -50
  49. package/dist/core/update-check.js +6 -6
  50. package/dist/core/version.js +1 -1
  51. package/dist/core/zellij/zellij-launcher.js +17 -5
  52. package/dist/core/zellij/zellij-slot-column-anchor.js +5 -1
  53. package/dist/scripts/check-dist-runtime.js +3 -2
  54. package/dist/scripts/codex-0142-manifest-check.js +2 -1
  55. package/dist/scripts/config-managed-merge-callsite-coverage-check.js +6 -0
  56. package/dist/scripts/doctor-dirty-plan-check.js +1 -1
  57. package/dist/scripts/doctor-transaction-engine-check.js +1 -0
  58. package/dist/scripts/doctor-warning-only-not-blocker-check.js +18 -1
  59. package/dist/scripts/loop-directive-check-lib.js +2 -1
  60. package/dist/scripts/mad-db-capability-check.js +13 -2
  61. package/dist/scripts/mad-db-command-check.js +7 -5
  62. package/dist/scripts/mad-db-hook-idempotency-check.js +21 -0
  63. package/dist/scripts/mad-db-ledger-check.js +2 -1
  64. package/dist/scripts/mad-db-lifecycle-hook-decision-check.js +5 -4
  65. package/dist/scripts/mad-db-mad-command-check.js +29 -16
  66. package/dist/scripts/mad-db-mcp-result-lifecycle-check.js +11 -10
  67. package/dist/scripts/mad-db-one-cycle-bounded-check.js +15 -18
  68. package/dist/scripts/mad-db-one-cycle-consumption-check.js +3 -3
  69. package/dist/scripts/mad-db-operation-lifecycle-blackbox.js +9 -9
  70. package/dist/scripts/mad-db-operation-lifecycle-ledger-check.js +6 -6
  71. package/dist/scripts/mad-db-parallel-lifecycle-check.js +24 -0
  72. package/dist/scripts/mad-db-policy-v2-check.js +20 -0
  73. package/dist/scripts/mad-db-priority-resolver-check.js +5 -5
  74. package/dist/scripts/mad-db-real-supabase-e2e.js +166 -0
  75. package/dist/scripts/mad-db-route-identity-check.js +28 -0
  76. package/dist/scripts/mad-db-runtime-profile-lifecycle-check.js +24 -0
  77. package/dist/scripts/mad-db-safety-conflict-matrix-check.js +3 -3
  78. package/dist/scripts/mad-db-skill-policy-snapshot-check.js +15 -0
  79. package/dist/scripts/mad-sks-zellij-launch-check.js +7 -1
  80. package/dist/scripts/managed-role-manifest-parity-check.js +4 -1
  81. package/dist/scripts/naruto-real-parallelism-blackbox.js +17 -4
  82. package/dist/scripts/native-capability-postcheck-check.js +1 -0
  83. package/dist/scripts/native-capability-repair-matrix-check.js +2 -0
  84. package/dist/scripts/native-chrome-web-review-repair-check.js +1 -0
  85. package/dist/scripts/native-computer-use-repair-check.js +1 -0
  86. package/dist/scripts/release-dag-full-coverage-check.js +6 -0
  87. package/dist/scripts/release-triwiki-first-runner-blackbox.js +5 -1
  88. package/dist/scripts/sks-3-1-5-directive-check-lib.js +1 -1
  89. package/dist/scripts/sks-401-all-feature-regression-blackbox.js +1 -1
  90. package/dist/scripts/update-concurrent-lock-check.js +1 -0
  91. package/dist/scripts/update-first-command-migration-check.js +4 -3
  92. package/package.json +13 -2
  93. package/schemas/mad-db/mad-db-capability.schema.json +92 -19
  94. package/schemas/update-migration.schema.json +13 -1
@@ -5,7 +5,8 @@ import { managedAgentRoleConfigForFile, managedAgentRoleConfigForRole } from '..
5
5
  export async function repairAgentConfigFileReferences(input) {
6
6
  const root = path.resolve(input.root);
7
7
  const configPath = path.join(root, '.codex', 'config.toml');
8
- const original = await fs.readFile(configPath, 'utf8').catch(() => '');
8
+ const configExists = await fs.stat(configPath).then((stat) => stat.isFile()).catch(() => false);
9
+ const original = configExists ? await fs.readFile(configPath, 'utf8').catch(() => '') : minimalManagedConfigToml();
9
10
  const createdFiles = [];
10
11
  const repairedPaths = [];
11
12
  const removedUnsupportedFields = [];
@@ -38,7 +39,12 @@ export async function repairAgentConfigFileReferences(input) {
38
39
  }
39
40
  if (edits.length)
40
41
  text = applyEdits(original, edits);
41
- if (input.apply && text !== original) {
42
+ if (input.apply && !configExists) {
43
+ await ensureDir(path.dirname(configPath));
44
+ await writeTextAtomic(configPath, text.replace(/\n{3,}/g, '\n\n').replace(/\s*$/, '\n'));
45
+ createdFiles.push(configPath);
46
+ }
47
+ else if (input.apply && text !== original) {
42
48
  await writeTextAtomic(configPath, text.replace(/\n{3,}/g, '\n\n').replace(/\s*$/, '\n'));
43
49
  }
44
50
  const effectiveText = input.apply ? await fs.readFile(configPath, 'utf8').catch(() => text) : text;
@@ -154,4 +160,39 @@ function escapeToml(value) {
154
160
  function escapeRegExp(value) {
155
161
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
156
162
  }
163
+ function minimalManagedConfigToml() {
164
+ return [
165
+ 'model = "gpt-5.5"',
166
+ 'model_reasoning_effort = "medium"',
167
+ 'service_tier = "fast"',
168
+ '',
169
+ '[features]',
170
+ 'hooks = true',
171
+ 'remote_control = true',
172
+ 'multi_agent = true',
173
+ 'fast_mode = true',
174
+ '',
175
+ '[mcp_servers.context7]',
176
+ 'url = "https://mcp.context7.com/mcp"',
177
+ '',
178
+ agentConfigBlock('native_agent', 'Read-only SKS analysis agent.', './agents/native-agent-intake.toml', ['Analysis', 'Mapper']),
179
+ '',
180
+ agentConfigBlock('team_consensus', 'SKS planning/debate agent.', './agents/team-consensus.toml', ['Consensus', 'Atlas']),
181
+ '',
182
+ agentConfigBlock('implementation_worker', 'SKS bounded implementation worker.', './agents/implementation-worker.toml', ['Builder', 'Mason']),
183
+ '',
184
+ agentConfigBlock('db_safety_reviewer', 'Read-only DB safety reviewer.', './agents/db-safety-reviewer.toml', ['Sentinel', 'Ledger']),
185
+ '',
186
+ agentConfigBlock('qa_reviewer', 'Read-only QA reviewer.', './agents/qa-reviewer.toml', ['Verifier', 'Reviewer']),
187
+ ''
188
+ ].join('\n');
189
+ }
190
+ function agentConfigBlock(table, description, configFile, nicknames = []) {
191
+ return [
192
+ `[agents.${table}]`,
193
+ `description = "${description}"`,
194
+ `config_file = "${configFile}"`,
195
+ `nickname_candidates = [${nicknames.map((name) => `"${name}"`).join(', ')}]`
196
+ ].join('\n');
197
+ }
157
198
  //# sourceMappingURL=agent-config-file-repair.js.map
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
- import { nowIso, writeJsonAtomic, writeTextAtomic, ensureDir } from '../fsx.js';
4
+ import { PACKAGE_VERSION, nowIso, writeJsonAtomic, writeTextAtomic, ensureDir } from '../fsx.js';
5
5
  import { repairAgentRoleConfigs } from '../agents/agent-role-config.js';
6
6
  import { agentRolePayloadFor, probeCodexAgentTypeSupport } from './codex-agent-type-probe.js';
7
7
  const DIRECTIVE_ROLES = [
@@ -86,12 +86,12 @@ export async function syncCodexAgentRoles(input) {
86
86
  function roleToml(role, payload) {
87
87
  return [
88
88
  `name = "${role}"`,
89
- `description = "SKS managed 4.1.0 directive role: ${role}"`,
89
+ `description = "SKS managed ${PACKAGE_VERSION} directive role: ${role}"`,
90
90
  'model_reasoning_effort = "medium"',
91
91
  role.includes('implementer') ? 'sandbox_mode = "workspace-write"' : 'sandbox_mode = "read-only"',
92
92
  'approval_policy = "never"',
93
93
  'developer_instructions = """',
94
- `You are ${role}. SKS managed 4.1.0 directive role with bounded ownership.`,
94
+ `You are ${role}. SKS managed ${PACKAGE_VERSION} directive role with bounded ownership.`,
95
95
  'Bounded ownership: use only the assigned owner files/directories and treat memory as guidance, not permission.',
96
96
  role.includes('implementer') ? 'Maker/checker separation: implementer may patch only owner scope and cannot self-approve.' : 'Maker/checker separation: checker is read-only and must reject missing gates or missing proof artifacts.',
97
97
  role.includes('implementer') ? 'Allowed sandbox: workspace-write only within assigned owner scope.' : 'Allowed sandbox: read-only; checker roles cannot mutate.',
@@ -107,7 +107,7 @@ function roleToml(role, payload) {
107
107
  ].join('\n');
108
108
  }
109
109
  function isSksManagedDirectiveRole(text) {
110
- return /SKS managed (?:3\.1\.(?:4|5|6|7|11)|4\.1\.0) (?:directive|bounded) role/.test(text)
110
+ return /SKS managed (?:3\.1\.(?:4|5|6|7|11)|4\.1\.\d+) (?:directive|bounded) role/.test(text)
111
111
  || /\bmessage_role_prefix\s*=/.test(text) && /SKS managed 3\.1\./.test(text);
112
112
  }
113
113
  function blockersOf(value) {
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { CURRENT_CODEX_RELEASE_MANIFEST } from '../codex-compat/codex-release-manifest.js';
5
5
  import { compareSemverLike } from '../codex-compat/codex-version-policy.js';
6
- import { ensureDir, nowIso, runProcess, sha256, writeJsonAtomic } from '../fsx.js';
6
+ import { ensureDir, nowIso, packageRoot, readJson, runProcess, sha256, writeJsonAtomic } from '../fsx.js';
7
7
  import { resolveCodexRuntime } from '../codex-runtime/resolve-codex-runtime.js';
8
8
  export const CODEX_0142_FEATURE_KEYS = [
9
9
  'runtime_identity',
@@ -31,8 +31,9 @@ export async function detectCodex0142Capability(input = {}) {
31
31
  return blockedCapability('blocked', null, null, [...runtime.blockers]);
32
32
  }
33
33
  const versionOk = compareSemverLike(runtime.identity.version, CURRENT_CODEX_RELEASE_MANIFEST.requiredCliVersion) >= 0;
34
- const schemaProbe = await generateSchemaProbe(root, runtime.identity.realpath);
35
- const appServerClientText = await fsp.readFile(path.join(root, 'src', 'core', 'codex-control', 'codex-app-server-v2-client.ts'), 'utf8').catch(() => '');
34
+ const schemaProbe = await schemaProbeForMode(root, runtime.identity.realpath, input.requireReal === true);
35
+ const implementationRoot = packageRoot();
36
+ const appServerClientText = await fsp.readFile(path.join(implementationRoot, 'src', 'core', 'codex-control', 'codex-app-server-v2-client.ts'), 'utf8').catch(() => '');
36
37
  const states = featureStatesFromSchema(schemaProbe.text, schemaProbe.ok, appServerClientText);
37
38
  const blockers = [
38
39
  ...(versionOk ? [] : ['codex_0_142_required']),
@@ -40,6 +41,7 @@ export async function detectCodex0142Capability(input = {}) {
40
41
  ...Object.values(states).flatMap((state) => state.blockers)
41
42
  ];
42
43
  const realEnough = blockers.length === 0
44
+ && schemaProbe.mode === 'real-schema'
43
45
  && schemaProbe.sha256 !== null
44
46
  && CURRENT_CODEX_RELEASE_MANIFEST.generatedSchemaSha256 !== 'pending-generated-schema'
45
47
  && schemaProbe.sha256 === CURRENT_CODEX_RELEASE_MANIFEST.generatedSchemaSha256
@@ -54,7 +56,7 @@ export async function detectCodex0142Capability(input = {}) {
54
56
  runtime_identity: runtime.identity,
55
57
  generated_schema_sha256: schemaProbe.sha256,
56
58
  manifest_schema_sha256: CURRENT_CODEX_RELEASE_MANIFEST.generatedSchemaSha256,
57
- probe_mode: schemaProbe.ok ? 'real-schema' : 'blocked',
59
+ probe_mode: schemaProbe.ok ? schemaProbe.mode : 'blocked',
58
60
  feature_states: states,
59
61
  blockers,
60
62
  warnings: [
@@ -133,7 +135,37 @@ function currentTimeState(schemaTextLower, schemaOk, appServerClientText) {
133
135
  blockers: clientHandlesCurrentTime ? [] : ['codex_0142_current_time_read_handler_not_verified']
134
136
  };
135
137
  }
138
+ async function schemaProbeForMode(root, codexBin, requireReal) {
139
+ if (!requireReal && process.env.SKS_CODEX_0142_REFRESH !== '1') {
140
+ const shipped = await shippedSchemaProbe();
141
+ if (shipped.ok)
142
+ return shipped;
143
+ }
144
+ return generateSchemaProbe(root, codexBin);
145
+ }
146
+ async function shippedSchemaProbe() {
147
+ const schemaRoot = path.join(packageRoot(), 'schemas', 'codex', 'app-server-0.142');
148
+ const files = await listFiles(schemaRoot).catch(() => []);
149
+ if (!files.length)
150
+ return { ok: false, text: 'shipped schema cache missing', sha256: null, mode: 'shipped-manifest-cache' };
151
+ const rows = await Promise.all(files.map(async (file) => {
152
+ const text = await fsp.readFile(file, 'utf8');
153
+ return {
154
+ relative: path.relative(schemaRoot, file),
155
+ text,
156
+ canonicalText: canonicalSchemaContent(file, text)
157
+ };
158
+ }));
159
+ const joined = rows.map((row) => `${row.relative}\n${row.text}`).join('\n');
160
+ const canonicalJoined = rows.map((row) => `${row.relative}\n${row.canonicalText}`).join('\n');
161
+ return { ok: true, text: joined, sha256: sha256(canonicalJoined), mode: 'shipped-manifest-cache' };
162
+ }
136
163
  async function generateSchemaProbe(root, codexBin) {
164
+ const cachePath = await schemaCachePath(root, codexBin);
165
+ const cached = await readJson(cachePath, null).catch(() => null);
166
+ if (cached?.ok === true && typeof cached.text === 'string' && cached.sha256) {
167
+ return { ok: true, text: cached.text, sha256: cached.sha256, mode: 'real-schema' };
168
+ }
137
169
  const out = path.join(os.tmpdir(), `sks-codex-0142-schema-${process.pid}-${Date.now()}`);
138
170
  await ensureDir(out);
139
171
  const result = await runProcess(codexBin, ['app-server', 'generate-json-schema', '--out', out], {
@@ -150,7 +182,7 @@ async function generateSchemaProbe(root, codexBin) {
150
182
  timedOut: false
151
183
  }));
152
184
  if (result.code !== 0)
153
- return { ok: false, text: `${result.stdout}\n${result.stderr}`, sha256: null };
185
+ return { ok: false, text: `${result.stdout}\n${result.stderr}`, sha256: null, mode: 'real-schema' };
154
186
  const files = await listFiles(out);
155
187
  const rows = await Promise.all(files.map(async (file) => {
156
188
  const text = await fsp.readFile(file, 'utf8');
@@ -163,7 +195,20 @@ async function generateSchemaProbe(root, codexBin) {
163
195
  const joined = rows.map((row) => `${row.relative}\n${row.text}`).join('\n');
164
196
  const canonicalJoined = rows.map((row) => `${row.relative}\n${row.canonicalText}`).join('\n');
165
197
  await fsp.rm(out, { recursive: true, force: true }).catch(() => { });
166
- return { ok: true, text: joined, sha256: sha256(canonicalJoined) };
198
+ const probe = { ok: true, text: joined, sha256: sha256(canonicalJoined), mode: 'real-schema' };
199
+ await writeJsonAtomic(cachePath, probe).catch(() => undefined);
200
+ return probe;
201
+ }
202
+ async function schemaCachePath(root, codexBin) {
203
+ const realpath = await fsp.realpath(codexBin).catch(() => codexBin);
204
+ const key = sha256(JSON.stringify({
205
+ realpath,
206
+ target: CURRENT_CODEX_RELEASE_MANIFEST.targetTag,
207
+ manifest_schema_sha256: CURRENT_CODEX_RELEASE_MANIFEST.generatedSchemaSha256,
208
+ platform: process.platform,
209
+ arch: process.arch
210
+ }));
211
+ return path.join(root, '.sneakoscope', 'cache', 'codex-0142-schema', `${key}.json`);
167
212
  }
168
213
  function canonicalSchemaContent(file, text) {
169
214
  if (!file.endsWith('.json'))
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { nowIso } from '../fsx.js';
2
+ import { nowIso, PACKAGE_VERSION } from '../fsx.js';
3
3
  import { resolveCodexRuntime } from '../codex-runtime/resolve-codex-runtime.js';
4
4
  export class CodexAppServerV2Client {
5
5
  command;
@@ -28,7 +28,7 @@ export class CodexAppServerV2Client {
28
28
  clientInfo: {
29
29
  name: 'sneakoscope-codex-app-server-v2',
30
30
  title: 'Sneakoscope Codex app-server v2',
31
- version: '4.1.0'
31
+ version: PACKAGE_VERSION
32
32
  },
33
33
  capabilities: {
34
34
  experimentalApi: true,
@@ -16,11 +16,34 @@ import { codexNativeFeatureState, computeCodexNativeInvocationDefaults } from '.
16
16
  const REPORT_PATH = '.sneakoscope/reports/codex-native-feature-matrix.json';
17
17
  const REQUIRED_SKILL_NAMES = MANAGED_SKILLS.map((skill) => skill.id);
18
18
  const REQUIRED_AGENT_ROLES = MANAGED_AGENT_ROLES.map((role) => role.id);
19
+ const invocationMatrixCache = new Map();
19
20
  export async function buildCodexNativeFeatureMatrix(input = { root: process.cwd() }) {
20
21
  const root = path.resolve(input.root || process.cwd());
22
+ if (input.snapshot) {
23
+ await writeCodexNativeFeatureMatrix(root, input.snapshot, input.missionDir);
24
+ return input.snapshot;
25
+ }
21
26
  const deprecatedApplyRepairs = input.applyRepairs === true;
22
27
  const mode = input.mode || (deprecatedApplyRepairs || input.repairManagedAssets === true ? 'repair' : 'read-only');
23
28
  const repairManagedAssets = mode === 'repair' && (input.repairManagedAssets === true || deprecatedApplyRepairs);
29
+ const managedAssetFingerprint = await readManagedAssetFingerprint(root);
30
+ const cacheKey = JSON.stringify({
31
+ root,
32
+ mode,
33
+ repairManagedAssets,
34
+ codexHome: process.env.CODEX_HOME || null,
35
+ managedAssetFingerprint,
36
+ fixture: [
37
+ process.env.SKS_CODEX_0138_FAKE,
38
+ process.env.SKS_CODEX_0139_FAKE,
39
+ process.env.SKS_CODEX_0140_FAKE,
40
+ process.env.SKS_CODEX_0142_FAKE,
41
+ process.env.SKS_CODEX_PLUGIN_JSON_FAKE
42
+ ]
43
+ });
44
+ if (!input.missionDir && !repairManagedAssets && invocationMatrixCache.has(cacheKey)) {
45
+ return invocationMatrixCache.get(cacheKey);
46
+ }
24
47
  const fixtureMode = process.env.SKS_CODEX_0138_FAKE === '1' || process.env.SKS_CODEX_0139_FAKE === '1' || process.env.SKS_CODEX_0140_FAKE === '1' || process.env.SKS_CODEX_0142_FAKE === '1' || process.env.SKS_CODEX_PLUGIN_JSON_FAKE === '1';
25
48
  const codexBin = fixtureMode ? process.env.CODEX_BIN || 'codex' : await findCodexBinary().catch(() => null);
26
49
  const version = codexBin ? await codexVersion(codexBin) : null;
@@ -190,6 +213,8 @@ export async function buildCodexNativeFeatureMatrix(input = { root: process.cwd(
190
213
  invocation_defaults: computeCodexNativeInvocationDefaults(matrixBase)
191
214
  };
192
215
  await writeCodexNativeFeatureMatrix(root, matrix, input.missionDir);
216
+ if (!input.missionDir && !repairManagedAssets)
217
+ invocationMatrixCache.set(cacheKey, matrix);
193
218
  return matrix;
194
219
  }
195
220
  async function inspectManagedSkillState(root) {
@@ -252,6 +277,31 @@ async function inspectManagedAgentRoleState(root) {
252
277
  warnings: existingCount > managed.size ? ['non_sks_agent_roles_ignored'] : []
253
278
  };
254
279
  }
280
+ async function readManagedAssetFingerprint(root) {
281
+ const dirs = [
282
+ path.join(root, '.agents', 'skills'),
283
+ path.join(root, '.codex', 'agents'),
284
+ ...(process.env.CODEX_HOME ? [path.join(process.env.CODEX_HOME, 'skills'), path.join(process.env.CODEX_HOME, 'agents')] : [])
285
+ ];
286
+ const rows = [];
287
+ for (const dir of dirs) {
288
+ const stat = await fs.stat(dir).catch(() => null);
289
+ rows.push(`${dir}:${stat ? `${stat.mtimeMs}:${stat.size}` : 'missing'}`);
290
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
291
+ for (const entry of entries) {
292
+ const file = path.join(dir, entry.name);
293
+ const childStat = await fs.stat(file).catch(() => null);
294
+ rows.push(`${file}:${entry.isDirectory() ? 'dir' : 'file'}:${childStat ? `${childStat.mtimeMs}:${childStat.size}` : 'missing'}`);
295
+ if (entry.isDirectory()) {
296
+ const skillFile = path.join(file, 'SKILL.md');
297
+ const skillStat = await fs.stat(skillFile).catch(() => null);
298
+ if (skillStat)
299
+ rows.push(`${skillFile}:file:${skillStat.mtimeMs}:${skillStat.size}`);
300
+ }
301
+ }
302
+ }
303
+ return rows;
304
+ }
255
305
  export async function writeCodexNativeFeatureMatrix(root, matrix, missionDir) {
256
306
  await writeJsonAtomic(path.join(root, REPORT_PATH), matrix);
257
307
  if (missionDir)
@@ -7,13 +7,19 @@ export async function postcheckNativeCapabilities(input) {
7
7
  const fixture = input.fixture || false;
8
8
  const matrix = input.matrix || await buildNativeCapabilityRepairMatrix({ root, fixture, reportPath: null });
9
9
  const capabilities = await Promise.all(matrix.capabilities.map((state) => postcheckCapability(root, state, fixture)));
10
- const blockers = capabilities.flatMap((state) => state.after === 'verified' || state.after === 'degraded' ? [] : state.blockers);
10
+ const coreBlockers = capabilities.flatMap((state) => state.core_blockers || []);
11
+ const routeBlockers = mergeRouteBlockers(capabilities);
11
12
  const checked = {
12
13
  ...matrix,
13
14
  generated_at: new Date().toISOString(),
14
- ok: blockers.length === 0,
15
+ ok: coreBlockers.length === 0,
15
16
  capabilities,
16
- blockers,
17
+ core_blockers: coreBlockers,
18
+ route_blockers: routeBlockers,
19
+ optional_manual_required: capabilities
20
+ .filter((state) => state.availability === 'manual-required' && state.after !== 'verified')
21
+ .map((state) => state.id),
22
+ blockers: coreBlockers,
17
23
  warnings: capabilities.flatMap((state) => state.warnings)
18
24
  };
19
25
  const reportPath = input.reportPath === null
@@ -48,17 +54,19 @@ function postcheckImageGeneration(state, fixture) {
48
54
  return {
49
55
  ...state,
50
56
  after: 'unknown',
51
- blockers: ['imagegen_auth_or_codex_app_builtin_missing'],
57
+ core_blockers: [],
58
+ route_blockers: mergeStateRouteBlockers(state, 'route-image', ['imagegen_auth_or_codex_app_builtin_missing']),
59
+ blockers: [],
52
60
  warnings: [...new Set([...state.warnings, 'image_generation_not_verified_without_real_capability'])]
53
61
  };
54
62
  }
55
63
  async function postcheckImageFollowupEdit(root, state) {
56
64
  const contract = await validateSavedArtifactPathContract(root);
57
65
  if (!contract.ok)
58
- return { ...state, after: 'blocked', blockers: contract.blockers };
66
+ return routeBlocked(state, 'route-image', contract.blockers);
59
67
  const sample = path.join(contract.imageArtifacts, 'postcheck-followup-sample.txt');
60
68
  if (!(await writeReadSample(sample)))
61
- return { ...state, after: 'blocked', blockers: ['image_followup_sample_artifact_unwritable'] };
69
+ return routeBlocked(state, 'route-image', ['image_followup_sample_artifact_unwritable']);
62
70
  return verified(state);
63
71
  }
64
72
  function postcheckComputerUse(state, _fixture) {
@@ -67,7 +75,9 @@ function postcheckComputerUse(state, _fixture) {
67
75
  return {
68
76
  ...state,
69
77
  after: 'unknown',
70
- blockers: ['computer_use_os_permission_or_capability_unknown'],
78
+ core_blockers: [],
79
+ route_blockers: mergeStateRouteBlockers(state, 'route-computer-use', ['computer_use_os_permission_or_capability_unknown']),
80
+ blockers: [],
71
81
  warnings: [...new Set([...state.warnings, 'manual_os_permission_required'])]
72
82
  };
73
83
  }
@@ -77,7 +87,9 @@ function postcheckChromeWebReview(state, fixture) {
77
87
  return {
78
88
  ...state,
79
89
  after: 'unknown',
80
- blockers: ['codex_chrome_extension_readiness_not_verified'],
90
+ core_blockers: [],
91
+ route_blockers: mergeStateRouteBlockers(state, 'route-chrome-web-review', ['codex_chrome_extension_readiness_not_verified']),
92
+ blockers: [],
81
93
  warnings: [...new Set([...state.warnings, 'manual_chrome_extension_setup_required'])]
82
94
  };
83
95
  }
@@ -88,12 +100,12 @@ async function postcheckAppScreenshot(root, state) {
88
100
  const dir = path.join(root, '.sneakoscope', 'app-screenshots');
89
101
  const registry = path.join(dir, 'screenshot-registry.json');
90
102
  if (!(await writeReadSample(path.join(dir, 'postcheck-screenshot-sample.txt')))) {
91
- return { ...state, after: 'blocked', blockers: ['app_screenshot_directory_unwritable'] };
103
+ return routeBlocked(state, 'route-image', ['app_screenshot_directory_unwritable']);
92
104
  }
93
105
  await writeJsonAtomic(registry, { schema: 'sks.app-screenshot-registry.v1', generated_at: new Date().toISOString(), screenshots: [] }).catch(() => undefined);
94
106
  const json = await readJson(registry, {}).catch(() => ({}));
95
107
  if (json.schema !== 'sks.app-screenshot-registry.v1')
96
- return { ...state, after: 'blocked', blockers: ['app_screenshot_registry_invalid'] };
108
+ return routeBlocked(state, 'route-image', ['app_screenshot_registry_invalid']);
97
109
  return verified(state);
98
110
  }
99
111
  function postcheckAppHandoff(state, fixture) {
@@ -102,7 +114,9 @@ function postcheckAppHandoff(state, fixture) {
102
114
  return {
103
115
  ...state,
104
116
  after: 'unknown',
105
- blockers: ['codex_app_handoff_not_verified'],
117
+ core_blockers: [],
118
+ route_blockers: mergeStateRouteBlockers(state, 'route-app-handoff', ['codex_app_handoff_not_verified']),
119
+ blockers: [],
106
120
  warnings: [...new Set([...state.warnings, 'manual_app_handoff_approval_required'])]
107
121
  };
108
122
  }
@@ -114,24 +128,53 @@ async function postcheckImagePathExposure(root, state, fixture) {
114
128
  return {
115
129
  ...state,
116
130
  after: 'degraded',
131
+ availability: 'available-unverified',
132
+ core_blockers: [],
133
+ route_blockers: {},
117
134
  blockers: [],
118
135
  warnings: [...new Set([...state.warnings, 'using_saved_artifact_path_contract_fallback'])]
119
136
  };
120
137
  }
121
- return { ...state, after: 'blocked', blockers: ['image_path_exposure_missing_without_fallback_contract', ...contract.blockers] };
138
+ return routeBlocked(state, 'route-image', ['image_path_exposure_missing_without_fallback_contract', ...contract.blockers]);
122
139
  }
123
140
  async function postcheckSavedArtifactPathContract(root, state) {
124
141
  const contract = await validateSavedArtifactPathContract(root);
125
142
  if (!contract.ok)
126
- return { ...state, after: 'blocked', blockers: contract.blockers };
143
+ return routeBlocked(state, 'route-image', contract.blockers);
127
144
  if (!(await writeReadSample(path.join(contract.imageArtifacts, 'postcheck-contract-image.txt'))))
128
- return { ...state, after: 'blocked', blockers: ['image_artifacts_directory_unwritable'] };
145
+ return routeBlocked(state, 'route-image', ['image_artifacts_directory_unwritable']);
129
146
  if (!(await writeReadSample(path.join(contract.appScreenshots, 'postcheck-contract-screenshot.txt'))))
130
- return { ...state, after: 'blocked', blockers: ['app_screenshots_directory_unwritable'] };
147
+ return routeBlocked(state, 'route-image', ['app_screenshots_directory_unwritable']);
131
148
  return verified(state);
132
149
  }
133
150
  function verified(state) {
134
- return { ...state, after: 'verified', blockers: [] };
151
+ return { ...state, after: 'verified', availability: 'verified', core_blockers: [], route_blockers: {}, blockers: [] };
152
+ }
153
+ function routeBlocked(state, scope, blockers) {
154
+ return {
155
+ ...state,
156
+ after: 'blocked',
157
+ core_blockers: [],
158
+ route_blockers: mergeStateRouteBlockers(state, scope, blockers),
159
+ blockers: []
160
+ };
161
+ }
162
+ function mergeStateRouteBlockers(state, scope, blockers) {
163
+ return {
164
+ ...(state.route_blockers || {}),
165
+ [scope]: [...new Set([...(state.route_blockers?.[scope] || []), ...blockers])]
166
+ };
167
+ }
168
+ function mergeRouteBlockers(states) {
169
+ const merged = {};
170
+ for (const state of states) {
171
+ for (const [scope, blockers] of Object.entries(state.route_blockers || {})) {
172
+ merged[scope] = [
173
+ ...new Set([...(merged[scope] || []), ...blockers])
174
+ ];
175
+ }
176
+ }
177
+ return merged;
135
178
  }
136
179
  async function validateSavedArtifactPathContract(root) {
137
180
  const contractPath = path.join(root, '.sneakoscope', 'reports', 'saved-artifact-path-contract.json');
@@ -24,14 +24,20 @@ export async function buildNativeCapabilityRepairMatrix(input) {
24
24
  ? fixtureNativeFeatureMatrix(fixture)
25
25
  : await buildCodexNativeFeatureMatrix({ root, mode: 'read-only' }).catch((err) => ({ ok: false, features: {}, blockers: [messageOf(err)], invocation_defaults: {} }));
26
26
  const states = await Promise.all(NATIVE_CAPABILITY_IDS.filter((id) => selected.has(id)).map((id) => stateForCapability(root, id, imageCapability, nativeFeatureMatrix)));
27
- const blockers = states.flatMap((state) => state.blockers);
27
+ const coreBlockers = states.flatMap((state) => state.core_blockers || state.blockers);
28
+ const routeBlockers = mergeRouteBlockers(states);
28
29
  const warnings = states.flatMap((state) => state.warnings);
29
30
  const matrix = {
30
31
  schema: 'sks.native-capability-repair-matrix.v1',
31
32
  generated_at: nowIso(),
32
- ok: blockers.length === 0,
33
+ ok: coreBlockers.length === 0,
33
34
  capabilities: states,
34
- blockers,
35
+ core_blockers: coreBlockers,
36
+ route_blockers: routeBlockers,
37
+ optional_manual_required: states
38
+ .filter((state) => state.availability === 'manual-required' && state.required_for.every((scope) => !isCoreScope(scope)))
39
+ .map((state) => state.id),
40
+ blockers: coreBlockers,
35
41
  warnings
36
42
  };
37
43
  const reportPath = input.reportPath === null
@@ -49,10 +55,15 @@ async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix
49
55
  id,
50
56
  before: verified ? 'verified' : 'blocked',
51
57
  repairability: verified ? 'auto' : 'manual-required',
52
- repair_actions: verified ? ['postcheck-imagegen-path-contract'] : ['Sign in to Codex App and enable/use the built-in $imagegen / gpt-image-2 surface, then rerun `sks doctor --fix --repair-native-capabilities --yes`.'],
58
+ availability: verified ? 'verified' : 'manual-required',
59
+ required_for: ['route-image'],
60
+ repair_actions: verified ? ['postcheck-imagegen-path-contract'] : ['Sign in to Codex App and enable/use the built-in $imagegen / gpt-image-2 surface, then rerun `sks doctor --capabilities --yes`.'],
53
61
  after: null,
54
62
  artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
55
- blockers: verified ? [] : ['imagegen_auth_or_codex_app_builtin_missing'],
63
+ core_blockers: [],
64
+ route_blockers: verified ? {} : { 'route-image': ['imagegen_auth_or_codex_app_builtin_missing'] },
65
+ manual_actions: verified ? [] : ['Sign in to Codex App and enable/use the built-in $imagegen / gpt-image-2 surface before image routes.'],
66
+ blockers: [],
56
67
  warnings: verified ? [] : ['image_generation_not_verified_without_real_capability']
57
68
  };
58
69
  }
@@ -75,10 +86,15 @@ async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix
75
86
  id,
76
87
  before: ok ? 'verified' : 'unknown',
77
88
  repairability: ok ? 'auto' : 'manual-required',
78
- repair_actions: ok ? ['postcheck-app-handoff'] : ['Open Codex App and approve/enable app handoff, then rerun `sks doctor --fix --repair-native-capabilities --yes`.'],
89
+ availability: ok ? 'verified' : 'manual-required',
90
+ required_for: ['route-app-handoff'],
91
+ repair_actions: ok ? ['postcheck-app-handoff'] : ['Open Codex App and approve/enable app handoff, then rerun `sks doctor --capabilities --yes`.'],
79
92
  after: null,
80
93
  artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
81
- blockers: ok ? [] : ['codex_app_handoff_not_verified'],
94
+ core_blockers: [],
95
+ route_blockers: ok ? {} : { 'route-app-handoff': ['codex_app_handoff_not_verified'] },
96
+ manual_actions: ok ? [] : ['Open Codex App and approve/enable app handoff before app handoff routes.'],
97
+ blockers: [],
82
98
  warnings: ok ? [] : ['manual_app_handoff_approval_required']
83
99
  };
84
100
  }
@@ -89,10 +105,15 @@ async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix
89
105
  id,
90
106
  before: ok ? 'verified' : fallback ? 'degraded' : 'missing',
91
107
  repairability: ok ? 'auto' : 'doctor-fix',
108
+ availability: ok ? 'verified' : fallback ? 'available-unverified' : 'unavailable',
109
+ required_for: ['route-image'],
92
110
  repair_actions: ok ? ['postcheck-image-path-exposure'] : ['create-saved-artifact-path-contract'],
93
111
  after: null,
94
112
  artifact_path: path.join(reports, 'saved-artifact-path-contract.json'),
95
- blockers: ok || fallback ? [] : ['image_path_exposure_missing_without_fallback_contract'],
113
+ core_blockers: [],
114
+ route_blockers: ok || fallback ? {} : { 'route-image': ['image_path_exposure_missing_without_fallback_contract'] },
115
+ manual_actions: [],
116
+ blockers: [],
96
117
  warnings: ok ? [] : ['using_saved_artifact_path_contract_fallback']
97
118
  };
98
119
  }
@@ -101,11 +122,16 @@ async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix
101
122
  return {
102
123
  id,
103
124
  before: envVerified ? 'verified' : 'unknown',
125
+ availability: envVerified ? 'verified' : 'manual-required',
126
+ required_for: ['route-computer-use'],
104
127
  repairability: envVerified ? 'auto' : 'manual-required',
105
- repair_actions: envVerified ? ['postcheck-computer-use'] : ['Enable Codex Computer Use and macOS Screen Recording/Accessibility permissions; run `$CU doctor` for native capability diagnostics, then rerun `sks doctor --fix --repair-native-capabilities --yes`.'],
128
+ repair_actions: envVerified ? ['postcheck-computer-use'] : ['Enable Codex Computer Use and macOS Screen Recording/Accessibility permissions; run `$CU doctor` for native capability diagnostics, then rerun `sks doctor --capabilities --yes`.'],
106
129
  after: null,
107
130
  artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
108
- blockers: envVerified ? [] : ['computer_use_os_permission_or_capability_unknown'],
131
+ core_blockers: [],
132
+ route_blockers: envVerified ? {} : { 'route-computer-use': ['computer_use_os_permission_or_capability_unknown'] },
133
+ manual_actions: envVerified ? [] : ['Enable Codex Computer Use and macOS Screen Recording/Accessibility permissions before `$CU` routes.'],
134
+ blockers: [],
109
135
  warnings: envVerified ? [] : ['manual_os_permission_required']
110
136
  };
111
137
  }
@@ -113,11 +139,16 @@ async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix
113
139
  return {
114
140
  id,
115
141
  before: chromeReady ? 'verified' : 'unknown',
142
+ availability: chromeReady ? 'verified' : 'manual-required',
143
+ required_for: ['route-chrome-web-review'],
116
144
  repairability: chromeReady ? 'auto' : 'manual-required',
117
- repair_actions: chromeReady ? ['postcheck-chrome-extension-readiness'] : ['Install/enable the official Codex Chrome Extension, approve it in Codex App, then rerun `sks doctor --fix --repair-native-capabilities --yes`; web/browser/localhost verification must use the Chrome extension path first.'],
145
+ repair_actions: chromeReady ? ['postcheck-chrome-extension-readiness'] : ['Install/enable the official Codex Chrome Extension, approve it in Codex App, then rerun `sks doctor --capabilities --yes`; web/browser/localhost verification must use the Chrome extension path first.'],
118
146
  after: null,
119
147
  artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
120
- blockers: chromeReady ? [] : ['codex_chrome_extension_readiness_not_verified'],
148
+ core_blockers: [],
149
+ route_blockers: chromeReady ? {} : { 'route-chrome-web-review': ['codex_chrome_extension_readiness_not_verified'] },
150
+ manual_actions: chromeReady ? [] : ['Install/enable the official Codex Chrome Extension before browser/web review routes.'],
151
+ blockers: [],
121
152
  warnings: chromeReady ? [] : ['manual_chrome_extension_setup_required']
122
153
  };
123
154
  }
@@ -157,14 +188,47 @@ function autoState(id, ready, artifactPath, actions) {
157
188
  return {
158
189
  id,
159
190
  before: ready ? 'verified' : 'missing',
191
+ availability: ready ? 'verified' : 'available-unverified',
192
+ required_for: routeScopesForCapability(id),
160
193
  repairability: ready ? 'auto' : 'doctor-fix',
161
194
  repair_actions: ready ? [`postcheck-${id}`] : actions,
162
195
  after: null,
163
196
  artifact_path: artifactPath,
164
- blockers: ready ? [] : [`${id}_repair_required`],
197
+ core_blockers: [],
198
+ route_blockers: ready ? {} : routeBlockerForCapability(id, `${id}_repair_required`),
199
+ manual_actions: [],
200
+ blockers: [],
165
201
  warnings: []
166
202
  };
167
203
  }
204
+ function routeScopesForCapability(id) {
205
+ if (id === 'image_followup_edit' || id === 'saved_artifact_path_contract' || id === 'codex_app_screenshot')
206
+ return ['route-image'];
207
+ if (id === 'app_handoff')
208
+ return ['route-app-handoff'];
209
+ if (id === 'computer_use')
210
+ return ['route-computer-use'];
211
+ if (id === 'chrome_web_review')
212
+ return ['route-chrome-web-review'];
213
+ return [];
214
+ }
215
+ function routeBlockerForCapability(id, blocker) {
216
+ const scopes = routeScopesForCapability(id);
217
+ return Object.fromEntries(scopes.map((scope) => [scope, [blocker]]));
218
+ }
219
+ function mergeRouteBlockers(states) {
220
+ const merged = {};
221
+ for (const state of states) {
222
+ for (const [scope, blockers] of Object.entries(state.route_blockers || {})) {
223
+ const next = [...(merged[scope] || []), ...blockers];
224
+ merged[scope] = [...new Set(next)];
225
+ }
226
+ }
227
+ return merged;
228
+ }
229
+ function isCoreScope(scope) {
230
+ return scope === 'core-cli' || scope === 'mad-interactive' || scope === 'managed-migration';
231
+ }
168
232
  function featureOk(matrix, feature) {
169
233
  return matrix?.features?.[feature]?.ok === true;
170
234
  }