sneakoscope 2.0.17 → 3.0.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 (56) hide show
  1. package/README.md +135 -90
  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/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/commands/doctor.js +39 -1
  8. package/dist/commands/mad-sks.js +2 -0
  9. package/dist/commands/zellij.js +58 -1
  10. package/dist/core/agents/agent-effort-policy.js +7 -1
  11. package/dist/core/agents/agent-scheduler.js +32 -24
  12. package/dist/core/agents/native-cli-session-swarm.js +22 -2
  13. package/dist/core/codex-app/codex-app-handoff.js +98 -0
  14. package/dist/core/codex-app/codex-app-launcher.js +103 -0
  15. package/dist/core/codex-control/codex-0138-capability.js +102 -0
  16. package/dist/core/codex-control/codex-model-capabilities.js +62 -0
  17. package/dist/core/codex-control/codex-model-metadata.js +91 -0
  18. package/dist/core/codex-control/codex-sdk-config-policy.js +1 -1
  19. package/dist/core/codex-control/codex-task-runner.js +1 -1
  20. package/dist/core/codex-plugins/codex-plugin-cache.js +38 -0
  21. package/dist/core/codex-plugins/codex-plugin-diff.js +73 -0
  22. package/dist/core/codex-plugins/codex-plugin-json.js +176 -0
  23. package/dist/core/commands/mad-sks-command.js +8 -0
  24. package/dist/core/commands/naruto-command.js +30 -1
  25. package/dist/core/commands/qa-loop-command.js +147 -5
  26. package/dist/core/doctor/codex-0138-doctor.js +104 -0
  27. package/dist/core/doctor/doctor-readiness-matrix.js +11 -0
  28. package/dist/core/effort-orchestrator.js +9 -0
  29. package/dist/core/fsx.js +1 -1
  30. package/dist/core/hooks-runtime.js +6 -9
  31. package/dist/core/image/image-artifact-path-contract.js +101 -0
  32. package/dist/core/image/image-artifact-registry.js +33 -0
  33. package/dist/core/image-ux-review/imagegen-adapter.js +49 -17
  34. package/dist/core/mad-db/mad-db-result-lifecycle.js +71 -0
  35. package/dist/core/mcp/mcp-plugin-inventory.js +29 -0
  36. package/dist/core/mcp/mcp-server-policy.js +24 -0
  37. package/dist/core/qa-loop/qa-loop-app-handoff-confirmation.js +51 -0
  38. package/dist/core/qa-loop/qa-loop-budget-policy.js +37 -0
  39. package/dist/core/qa-loop.js +70 -3
  40. package/dist/core/release/release-gate-cache-v2.js +47 -5
  41. package/dist/core/usage/codex-account-usage.js +139 -0
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/zellij/zellij-slot-column-anchor.js +16 -7
  44. package/dist/core/zellij/zellij-slot-pane-renderer.js +23 -2
  45. package/dist/core/zellij/zellij-slot-telemetry.js +65 -12
  46. package/dist/core/zellij/zellij-ui-mode.js +8 -1
  47. package/dist/core/zellij/zellij-update.js +307 -0
  48. package/dist/core/zellij/zellij-worker-pane-manager.js +211 -145
  49. package/dist/scripts/release-gate-existence-audit.js +5 -1
  50. package/package.json +46 -3
  51. package/schemas/codex-app/codex-app-handoff.schema.json +20 -0
  52. package/schemas/codex-plugins/codex-plugin-inventory.schema.json +32 -0
  53. package/schemas/image/image-artifact-path-contract.schema.json +32 -0
  54. package/schemas/usage/codex-account-usage.schema.json +27 -0
  55. package/dist/core/naruto/naruto-work-stealing.js +0 -11
  56. package/dist/core/zellij/zellij-right-column-layout-proof.js +0 -42
@@ -0,0 +1,104 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { detectCodex0138Capability } from '../codex-control/codex-0138-capability.js';
5
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
6
+ export async function runCodex0138Doctor(root, input = {}) {
7
+ const capability = await detectCodex0138Capability();
8
+ const fixed = [];
9
+ const checks = {
10
+ bash_fallback: await bashFallbackCheck(),
11
+ linux_proxy_socket_path: linuxProxySocketCheck(root),
12
+ oauth_mcp_prerefresh: oauthMcpPrerefreshCheck(capability),
13
+ agents_logical_path: await agentsLogicalPathCheck(root),
14
+ plugin_discovery_cache: await pluginDiscoveryCacheCheck(root, input.fix === true, fixed)
15
+ };
16
+ const warnings = [
17
+ ...(capability.ok ? [] : ['codex_0_138_not_detected']),
18
+ ...Object.values(checks).flatMap((check) => Array.isArray(check.warnings) ? check.warnings : [])
19
+ ];
20
+ const blockers = Object.values(checks).flatMap((check) => Array.isArray(check.blockers) ? check.blockers : []);
21
+ const report = {
22
+ schema: 'sks.codex-0138-doctor.v1',
23
+ generated_at: nowIso(),
24
+ ok: blockers.length === 0,
25
+ codex_0138_capability: capability,
26
+ checks,
27
+ fixed,
28
+ warnings,
29
+ blockers
30
+ };
31
+ await writeJsonAtomic(path.join(root, '.sneakoscope', 'codex-0138-doctor.json'), report);
32
+ return report;
33
+ }
34
+ async function bashFallbackCheck() {
35
+ const candidates = ['/bin/bash', '/usr/bin/bash'];
36
+ const existing = [];
37
+ for (const candidate of candidates) {
38
+ try {
39
+ await fs.access(candidate);
40
+ existing.push(candidate);
41
+ }
42
+ catch { }
43
+ }
44
+ return {
45
+ ok: existing.length > 0,
46
+ candidates,
47
+ existing,
48
+ blockers: existing.length ? [] : ['bash_fallback_missing'],
49
+ warnings: []
50
+ };
51
+ }
52
+ function linuxProxySocketCheck(root) {
53
+ if (process.platform !== 'linux')
54
+ return { ok: true, status: 'not_linux', warnings: [], blockers: [] };
55
+ const candidate = path.join(os.tmpdir(), 'sks-proxy', path.basename(root), 'proxy.sock');
56
+ return {
57
+ ok: candidate.length < 100,
58
+ candidate,
59
+ length: candidate.length,
60
+ warnings: candidate.length < 100 ? [] : ['linux_proxy_socket_path_long'],
61
+ blockers: []
62
+ };
63
+ }
64
+ function oauthMcpPrerefreshCheck(capability) {
65
+ return {
66
+ ok: true,
67
+ supported: capability.supports_oauth_mcp_prerefresh === true,
68
+ warnings: capability.supports_oauth_mcp_prerefresh ? [] : ['oauth_mcp_prerefresh_requires_codex_0_138'],
69
+ blockers: []
70
+ };
71
+ }
72
+ async function agentsLogicalPathCheck(root) {
73
+ const agents = path.join(root, 'AGENTS.md');
74
+ const realRoot = await fs.realpath(root).catch(() => root);
75
+ const exists = await fs.stat(agents).then((st) => st.isFile()).catch(() => false);
76
+ return {
77
+ ok: exists,
78
+ logical_path: agents,
79
+ real_root: realRoot,
80
+ warnings: exists ? [] : ['agents_md_missing_or_unreadable'],
81
+ blockers: []
82
+ };
83
+ }
84
+ async function pluginDiscoveryCacheCheck(root, fix, fixed) {
85
+ const cacheDir = path.join(root, '.sneakoscope', 'cache', 'codex-plugin-discovery');
86
+ const exists = await fs.stat(cacheDir).then((st) => st.isDirectory()).catch(() => false);
87
+ if (!exists && fix) {
88
+ await fs.mkdir(cacheDir, { recursive: true });
89
+ await writeJsonAtomic(path.join(cacheDir, 'README.json'), {
90
+ schema: 'sks.codex-plugin-discovery-cache.v1',
91
+ repaired_at: nowIso(),
92
+ purpose: 'Codex 0.138 plugin discovery cache placeholder; safe to refresh from codex plugin list --json.'
93
+ });
94
+ fixed.push('plugin_discovery_cache');
95
+ }
96
+ const after = exists || fix;
97
+ return {
98
+ ok: after,
99
+ path: cacheDir,
100
+ warnings: after ? [] : ['plugin_discovery_cache_missing_repair_available'],
101
+ blockers: []
102
+ };
103
+ }
104
+ //# sourceMappingURL=codex-0138-doctor.js.map
@@ -52,6 +52,14 @@ export function buildDoctorReadinessMatrix(input = {}) {
52
52
  blockers.add('codex_app_fast_ui_repair_requires_confirmation');
53
53
  if (input.codex_app_ui?.fast_selector === 'repaired')
54
54
  warnings.add('codex_app_fast_selector_repaired_restart_app_if_needed');
55
+ const codex0138Doctor = input.codex_0138_doctor || null;
56
+ if (codex0138Doctor?.ok === false)
57
+ for (const blocker of normalizeList(codex0138Doctor.blockers))
58
+ warnings.add(blocker);
59
+ for (const warning of normalizeList(codex0138Doctor?.warnings))
60
+ warnings.add(warning);
61
+ for (const warning of normalizeList(input.codex_plugin_app_template_policy?.doctor_warnings))
62
+ warnings.add(warning);
55
63
  if (input.codex_lb?.ok === false)
56
64
  warnings.add(`codex_lb_${input.codex_lb?.circuit?.state || 'blocked'}`);
57
65
  const localModel = input.local_model || {};
@@ -107,6 +115,9 @@ export function buildDoctorReadinessMatrix(input = {}) {
107
115
  replacement: 'zellij'
108
116
  },
109
117
  codex_doctor: codexDoctor || null,
118
+ codex_0138_doctor: codex0138Doctor,
119
+ codex_plugin_inventory: input.codex_plugin_inventory || null,
120
+ codex_plugin_app_template_policy: input.codex_plugin_app_template_policy || null,
110
121
  fast_mode_ready: input.fast_mode_ready !== false,
111
122
  codex_app_ui: input.codex_app_ui || null,
112
123
  hooks_ready: input.hooks_ready !== false,
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { nowIso, writeJsonAtomic } from './fsx.js';
3
3
  import { ARTIFACT_FILES } from './artifact-schemas.js';
4
+ import { codexModelEffortCapability, modelEffortAtLeast } from './codex-control/codex-model-capabilities.js';
4
5
  export const EFFORT_POLICY_VERSION = 1;
5
6
  export function selectEffort(task = {}) {
6
7
  const route = String(task.route || task.command || '').toLowerCase();
@@ -39,11 +40,19 @@ export function selectEffort(task = {}) {
39
40
  selected = 'medium';
40
41
  reasonCodes.push('default_medium');
41
42
  }
43
+ const modelCapability = codexModelEffortCapability({
44
+ model: task.model || task.model_id,
45
+ advertisedEfforts: task.advertised_efforts || task.model_advertised_efforts,
46
+ defaultEffort: task.model_reasoning_effort || task.default_effort
47
+ });
48
+ const modelReasoningEffort = modelEffortAtLeast(selected, modelCapability);
42
49
  return {
43
50
  schema_version: EFFORT_POLICY_VERSION,
44
51
  mission_id: task.mission_id || 'unassigned',
45
52
  task_id: task.task_id || 'TASK-001',
46
53
  selected_effort: selected,
54
+ model_reasoning_effort: modelReasoningEffort,
55
+ model_effort_capability: modelCapability,
47
56
  reason_codes: reasonCodes,
48
57
  risk_scores: risks,
49
58
  demotion_allowed_after: demotionPolicy(selected),
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 = '2.0.17';
8
+ export const PACKAGE_VERSION = '3.0.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() {
@@ -3,7 +3,7 @@ import { projectRoot, readJson, readText, writeJsonAtomic, appendJsonl, readStdi
3
3
  import { looksInteractiveCommand, interactiveCommandReason } from './no-question-guard.js';
4
4
  import { missionDir, setCurrent, stateFile } from './mission.js';
5
5
  import { checkDbOperation, dbBlockReason, handleMadSksUserConfirmation } from './db-safety.js';
6
- import { readLatestPendingMadDbLifecycleHook, recordMadDbToolResult } from './mad-db/mad-db-result-lifecycle.js';
6
+ import { maybeRecordMadDbToolResultFromToolUse } from './mad-db/mad-db-result-lifecycle.js';
7
7
  import { checkHarnessModification, harnessGuardBlockReason, isHarnessSourceProject } from './harness-guard.js';
8
8
  import { isMadSksRouteState } from './permission-gates.js';
9
9
  import { classifyMadSksShellCommand } from './mad-sks/write-guard.js';
@@ -163,6 +163,8 @@ function toolFailed(payload = {}) {
163
163
  if (Number.isFinite(n))
164
164
  return n !== 0;
165
165
  }
166
+ if (payload.isError === true || payload.tool_response?.isError === true || payload.toolResponse?.isError === true || payload.result?.isError === true)
167
+ return true;
166
168
  if (payload.success === false || payload.tool_response?.success === false || payload.toolResponse?.success === false || payload.result?.success === false)
167
169
  return true;
168
170
  if (payload.executed === false)
@@ -450,16 +452,11 @@ async function hookPostTool(root, state, payload, noQuestion) {
450
452
  async function recordMadDbPostToolLifecycle(root, state = {}, payload = {}) {
451
453
  if (!state?.mission_id)
452
454
  return null;
453
- const hook = await readLatestPendingMadDbLifecycleHook(root, String(state.mission_id), payload);
454
- if (!hook)
455
- return null;
456
- return recordMadDbToolResult({
455
+ return maybeRecordMadDbToolResultFromToolUse({
457
456
  root,
458
457
  missionId: String(state.mission_id),
459
- hook,
460
- ok: !toolFailed(payload),
461
- rowCount: extractRowCount(payload),
462
- error: toolFailed(payload) ? extractToolError(payload) : null
458
+ toolCallPayload: payload,
459
+ toolResult: payload
463
460
  });
464
461
  }
465
462
  function extractRowCount(payload = {}) {
@@ -0,0 +1,101 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
4
+ import { imageDimensions } from '../wiki-image/image-hash.js';
5
+ export async function buildImageArtifactPathContract(root, input) {
6
+ const images = [];
7
+ const blockers = [];
8
+ for (const [index, image] of input.images.entries()) {
9
+ const filePath = path.resolve(root, image.filePath || '');
10
+ const exists = await fileExists(filePath);
11
+ if (!exists)
12
+ blockers.push(`${image.kind}_file_path_missing:${image.id || index + 1}`);
13
+ const dims = exists ? await imageDimensions(filePath).catch(() => null) : null;
14
+ images.push({
15
+ id: image.id || `image-${index + 1}`,
16
+ kind: image.kind,
17
+ file_path: filePath,
18
+ relative_path: path.relative(root, filePath),
19
+ route: image.route || null,
20
+ stage: image.stage || null,
21
+ exists,
22
+ mime_type: mimeForPath(filePath),
23
+ width: dims?.width ?? null,
24
+ height: dims?.height ?? null,
25
+ model_visible_path: filePath,
26
+ followup_edit_hint: exists
27
+ ? `Use this saved local path for follow-up image edits: ${filePath}`
28
+ : 'Image file path missing; do not run visual QA until a real saved file path exists.'
29
+ });
30
+ }
31
+ if (images.some((image) => image.kind === 'generated_image' && !image.exists))
32
+ blockers.push('image_generated_file_path_missing');
33
+ return {
34
+ schema: 'sks.image-artifact-path-contract.v1',
35
+ mission_id: input.missionId,
36
+ generated_at: nowIso(),
37
+ images,
38
+ blockers: [...new Set(blockers)]
39
+ };
40
+ }
41
+ export async function writeImageArtifactPathContract(root, input) {
42
+ const contract = await buildImageArtifactPathContract(root, input);
43
+ const artifactPath = input.artifactPath || path.join(root, '.sneakoscope', 'missions', input.missionId, 'image-artifact-path-contract.json');
44
+ await writeJsonAtomic(artifactPath, contract);
45
+ return { contract, artifact_path: artifactPath };
46
+ }
47
+ export async function discoverImageArtifactsInDir(dir) {
48
+ const out = [];
49
+ await walk(dir, async (file) => {
50
+ if (!/\.(png|jpe?g|webp|gif)$/i.test(file))
51
+ return;
52
+ out.push({
53
+ id: path.basename(file).replace(/[^0-9A-Za-z._-]/g, '_'),
54
+ kind: /generated|gpt-image|callout/i.test(file) ? 'generated_image' : 'visual_qa_snapshot',
55
+ filePath: file
56
+ });
57
+ });
58
+ return out;
59
+ }
60
+ function mimeForPath(file) {
61
+ const ext = path.extname(file).toLowerCase();
62
+ if (ext === '.png')
63
+ return 'image/png';
64
+ if (ext === '.jpg' || ext === '.jpeg')
65
+ return 'image/jpeg';
66
+ if (ext === '.webp')
67
+ return 'image/webp';
68
+ if (ext === '.gif')
69
+ return 'image/gif';
70
+ return null;
71
+ }
72
+ async function fileExists(file) {
73
+ try {
74
+ const st = await fs.stat(file);
75
+ return st.isFile();
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ async function walk(dir, visit) {
82
+ let entries;
83
+ try {
84
+ entries = await fs.readdir(dir, { withFileTypes: true });
85
+ }
86
+ catch {
87
+ return;
88
+ }
89
+ for (const entry of entries) {
90
+ const full = path.join(dir, entry.name);
91
+ if (entry.isDirectory()) {
92
+ if (['node_modules', '.git', 'dist'].includes(entry.name))
93
+ continue;
94
+ await walk(full, visit);
95
+ }
96
+ else {
97
+ await visit(full);
98
+ }
99
+ }
100
+ }
101
+ //# sourceMappingURL=image-artifact-path-contract.js.map
@@ -0,0 +1,33 @@
1
+ import path from 'node:path';
2
+ import { readJson, writeJsonAtomic } from '../fsx.js';
3
+ import { buildImageArtifactPathContract } from './image-artifact-path-contract.js';
4
+ export async function registerImageArtifact(root, input) {
5
+ const artifactPath = imageArtifactRegistryPath(root, input.missionId);
6
+ const existing = await readJson(artifactPath, null);
7
+ const id = input.id || path.basename(input.filePath).replace(/[^0-9A-Za-z._-]/g, '_');
8
+ const rows = [
9
+ ...(existing?.images || [])
10
+ .filter((image) => image.id !== id)
11
+ .map((image) => ({
12
+ id: image.id,
13
+ kind: image.kind,
14
+ filePath: image.file_path,
15
+ route: image.route || null,
16
+ stage: image.stage || null
17
+ })),
18
+ {
19
+ id,
20
+ kind: input.kind,
21
+ filePath: input.filePath,
22
+ route: input.route,
23
+ stage: input.stage
24
+ }
25
+ ];
26
+ const contract = await buildImageArtifactPathContract(root, { missionId: input.missionId, images: rows });
27
+ await writeJsonAtomic(artifactPath, contract);
28
+ return contract;
29
+ }
30
+ export function imageArtifactRegistryPath(root, missionId) {
31
+ return path.join(root, '.sneakoscope', 'missions', missionId, 'image-artifact-path-contract.json');
32
+ }
33
+ //# sourceMappingURL=image-artifact-registry.js.map
@@ -1,12 +1,14 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
3
  import { parseShellEnvValue } from '../codex-lb/codex-lb-env.js';
4
- import { ensureDir, exists, nowIso, readJson, readText, writeJsonAtomic } from '../fsx.js';
4
+ import { ensureDir, exists, nowIso, projectRoot, readJson, readText, writeJsonAtomic } from '../fsx.js';
5
5
  import { sha256File, imageDimensions } from '../wiki-image/image-hash.js';
6
6
  import { detectImagegenCapability } from '../imagegen/imagegen-capability.js';
7
7
  import { validateGptImage2Request } from '../imagegen/gpt-image-2-request-validator.js';
8
8
  import { withResponsesRetry } from '../responses-retry-policy.js';
9
9
  import { discoverCodexAppGeneratedImage } from './codex-app-generated-image-discovery.js';
10
+ import { writeImageArtifactPathContract } from '../image/image-artifact-path-contract.js';
11
+ import { registerImageArtifact } from '../image/image-artifact-registry.js';
10
12
  const DEFAULT_OPENAI_IMAGE_EDITS_ENDPOINT = 'https://api.openai.com/v1/images/edits';
11
13
  export function buildCalloutPrompt(sourceScreenId, context = {}) {
12
14
  return [
@@ -124,6 +126,7 @@ export function createCodexAppImagegenAdapter(opts = {}) {
124
126
  real_generated: true
125
127
  });
126
128
  const outputSource = manualOutput ? 'manual_attach' : 'auto_discovered_generated_images';
129
+ const imageContract = await writeGeneratedImagePathContract(input, dest, 'codex_app_imagegen').catch(() => null);
127
130
  await writeJsonAtomic(responseArtifact, {
128
131
  schema: 'sks.image-ux-gpt-image-2-response.v1',
129
132
  created_at: nowIso(),
@@ -135,6 +138,7 @@ export function createCodexAppImagegenAdapter(opts = {}) {
135
138
  output_image_sha256: meta.sha256,
136
139
  output_id: meta.output_id,
137
140
  output_source: outputSource,
141
+ image_artifact_path_contract: imageContract?.artifact_path || null,
138
142
  discovered_from: discovery?.selected?.path || null,
139
143
  discovery: discovery ? { candidates_considered: discovery.candidates_considered, since_ms: discovery.since_ms, max_age_ms: discovery.max_age_ms } : null,
140
144
  local_only: true
@@ -149,6 +153,7 @@ export function createCodexAppImagegenAdapter(opts = {}) {
149
153
  output_source: outputSource,
150
154
  request_artifact: requestArtifact,
151
155
  response_artifact: responseArtifact,
156
+ image_artifact_path_contract: imageContract?.artifact_path || null,
152
157
  latency_ms: null
153
158
  };
154
159
  }
@@ -241,6 +246,7 @@ export function createFakeImagegenAdapter(opts = {}) {
241
246
  real_generated: false,
242
247
  mock: true
243
248
  });
249
+ const imageContract = await writeGeneratedImagePathContract(input, out, 'fake_imagegen_adapter').catch(() => null);
244
250
  await writeJsonAtomic(responseArtifact, {
245
251
  schema: 'sks.image-ux-gpt-image-2-response.v1',
246
252
  created_at: nowIso(),
@@ -251,6 +257,7 @@ export function createFakeImagegenAdapter(opts = {}) {
251
257
  output_image_path: out,
252
258
  output_image_sha256: meta.sha256,
253
259
  output_id: meta.output_id,
260
+ image_artifact_path_contract: imageContract?.artifact_path || null,
254
261
  dimensions: { width: meta.width, height: meta.height, format: meta.format },
255
262
  latency_ms: Date.now() - started,
256
263
  fake_adapter: true,
@@ -259,7 +266,7 @@ export function createFakeImagegenAdapter(opts = {}) {
259
266
  mock: true,
260
267
  local_only: true
261
268
  });
262
- return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'fake_imagegen_adapter', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
269
+ return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'fake_imagegen_adapter', request_artifact: requestArtifact, response_artifact: responseArtifact, image_artifact_path_contract: imageContract?.artifact_path || null, latency_ms: Date.now() - started };
263
270
  }
264
271
  };
265
272
  }
@@ -385,6 +392,7 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
385
392
  output_id: generated.id || payload?.id || null,
386
393
  real_generated: true
387
394
  });
395
+ const imageContract = await writeGeneratedImagePathContract(input, out, 'openai_responses_image_generation').catch(() => null);
388
396
  await writeJsonAtomic(responseArtifact, {
389
397
  schema: 'sks.image-ux-gpt-image-2-response.v1',
390
398
  created_at: nowIso(),
@@ -397,12 +405,13 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
397
405
  output_image_path: out,
398
406
  output_image_sha256: meta.sha256,
399
407
  output_id: meta.output_id,
408
+ image_artifact_path_contract: imageContract?.artifact_path || null,
400
409
  dimensions: { width: meta.width, height: meta.height, format: meta.format },
401
410
  latency_ms: Date.now() - started,
402
411
  token_cost_metadata: payload?.usage || null,
403
412
  local_only: true
404
413
  });
405
- return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'openai_responses_image_generation', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
414
+ return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'openai_responses_image_generation', request_artifact: requestArtifact, response_artifact: responseArtifact, image_artifact_path_contract: imageContract?.artifact_path || null, latency_ms: Date.now() - started };
406
415
  }
407
416
  const sourceBytes = await fsp.readFile(sourcePath);
408
417
  const qualityParam = imagegenQualityParam(opts);
@@ -440,6 +449,7 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
440
449
  output_id: image?.id || payload?.id || null,
441
450
  real_generated: true
442
451
  });
452
+ const imageContract = await writeGeneratedImagePathContract(input, out, 'openai_images_api').catch(() => null);
443
453
  await writeJsonAtomic(responseArtifact, {
444
454
  schema: 'sks.image-ux-gpt-image-2-response.v1',
445
455
  created_at: nowIso(),
@@ -451,12 +461,13 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
451
461
  output_image_path: out,
452
462
  output_image_sha256: meta.sha256,
453
463
  output_id: meta.output_id,
464
+ image_artifact_path_contract: imageContract?.artifact_path || null,
454
465
  dimensions: { width: meta.width, height: meta.height, format: meta.format },
455
466
  latency_ms: Date.now() - started,
456
467
  token_cost_metadata: payload?.usage || null,
457
468
  local_only: true
458
469
  });
459
- return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'openai_images_api', request_artifact: requestArtifact, response_artifact: responseArtifact, latency_ms: Date.now() - started };
470
+ return { ok: true, status: 'generated', generated_image_path: out, output_id: meta.output_id, blocker: null, provider: 'openai_images_api', request_artifact: requestArtifact, response_artifact: responseArtifact, image_artifact_path_contract: imageContract?.artifact_path || null, latency_ms: Date.now() - started };
460
471
  }
461
472
  catch (err) {
462
473
  const provider = useResponsesImageTool ? 'openai_responses_image_generation' : 'openai_images_api';
@@ -468,6 +479,37 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
468
479
  }
469
480
  };
470
481
  }
482
+ async function writeGeneratedImagePathContract(input, outputPath, provider) {
483
+ const root = await resolveImageArtifactRoot(input);
484
+ if (input.mission_id) {
485
+ await registerImageArtifact(root, {
486
+ missionId: input.mission_id,
487
+ id: `${provider}-${input.source_screen_id || 'screen'}`,
488
+ kind: 'generated_image',
489
+ filePath: outputPath,
490
+ route: '$Image-UX-Review',
491
+ stage: provider
492
+ });
493
+ }
494
+ return writeImageArtifactPathContract(root, {
495
+ missionId: input.mission_id || 'unassigned',
496
+ images: [{
497
+ id: `${provider}-${input.source_screen_id || 'screen'}`,
498
+ kind: 'generated_image',
499
+ filePath: outputPath,
500
+ route: '$Image-UX-Review',
501
+ stage: provider
502
+ }],
503
+ artifactPath: path.join(input.output_dir, 'image-artifact-path-contract.json')
504
+ });
505
+ }
506
+ async function resolveImageArtifactRoot(input) {
507
+ const cwdRoot = await projectRoot(process.cwd()).catch(() => process.cwd());
508
+ const resolvedCwd = path.resolve(process.cwd());
509
+ if (path.resolve(cwdRoot) !== resolvedCwd)
510
+ return cwdRoot;
511
+ return projectRoot(input.output_dir || process.cwd()).catch(() => cwdRoot);
512
+ }
471
513
  export async function generateGptImage2CalloutReview(input, opts = {}) {
472
514
  if (opts.fake === true || process.env.SKS_TEST_FAKE_IMAGEGEN === '1') {
473
515
  return createFakeImagegenAdapter(opts.fakeAdapter || {}).generateCalloutReview(input);
@@ -479,21 +521,11 @@ export async function generateGptImage2CalloutReview(input, opts = {}) {
479
521
  // allowApiFallback:false or SKS_IMAGEGEN_ALLOW_API_FALLBACK=0.
480
522
  const openAiKeyPresent = Boolean(opts.openai?.apiKey || process.env.OPENAI_API_KEY);
481
523
  const explicitDisableApiFallback = opts.allowApiFallback === false || process.env.SKS_IMAGEGEN_ALLOW_API_FALLBACK === '0';
482
- // codex-lb imagegen routes gpt-image-2 through the same Codex /responses
483
- // backend the LB already proxies (base_url ends in /backend-api/codex, so the
484
- // image_generation tool call is just another Responses request). When codex-lb
485
- // is the active, fully-configured auth (selected provider + key + base_url) and
486
- // there is no direct OPENAI_API_KEY, enable it BY DEFAULT so image generation
487
- // works for users authenticated only through codex-lb — that is the common case
488
- // and a hard block here is the bug the user hit. It still never overrides a real
489
- // OpenAI key, and SKS_IMAGEGEN_ALLOW_CODEX_LB_API_FALLBACK=0 (or
490
- // allowCodexLbApiFallback:false) opts out for callers that require Codex App
491
- // built-in evidence only.
492
- const codexLbAuthActive = capability?.codex_lb?.available === true;
524
+ // codex-lb imagegen is a direct API fallback, not Codex App imagegen evidence.
525
+ // It must be explicitly enabled by the caller or environment.
493
526
  const explicitDisableCodexLbFallback = opts.allowCodexLbApiFallback === false || process.env.SKS_IMAGEGEN_ALLOW_CODEX_LB_API_FALLBACK === '0';
494
527
  const allowCodexLbApiFallback = !explicitDisableCodexLbFallback && (opts.allowCodexLbApiFallback === true
495
- || process.env.SKS_IMAGEGEN_ALLOW_CODEX_LB_API_FALLBACK === '1'
496
- || (codexLbAuthActive && !openAiKeyPresent));
528
+ || process.env.SKS_IMAGEGEN_ALLOW_CODEX_LB_API_FALLBACK === '1');
497
529
  const allowApiFallback = !explicitDisableApiFallback && (opts.allowApiFallback === true
498
530
  || process.env.SKS_IMAGEGEN_ALLOW_API_FALLBACK === '1'
499
531
  || openAiKeyPresent
@@ -74,6 +74,24 @@ export async function recordMadDbToolResult(input) {
74
74
  event
75
75
  };
76
76
  }
77
+ export async function maybeRecordMadDbToolResultFromToolUse(input) {
78
+ const payload = input.toolResult ?? input.toolCallPayload ?? {};
79
+ const hook = lifecycleHookFromUnknown(input.decision)
80
+ || lifecycleHookFromUnknown(input.toolCallPayload)
81
+ || lifecycleHookFromUnknown(input.toolResult)
82
+ || await readLatestPendingMadDbLifecycleHook(input.root, input.missionId, input.toolCallPayload || payload);
83
+ if (!hook)
84
+ return null;
85
+ const ok = !madDbToolUseFailed(payload);
86
+ return recordMadDbToolResult({
87
+ root: input.root,
88
+ missionId: input.missionId,
89
+ hook,
90
+ ok,
91
+ rowCount: extractRowCount(payload),
92
+ error: ok ? null : extractToolError(payload)
93
+ });
94
+ }
77
95
  export function lifecycleHookFromUnknown(value) {
78
96
  const candidate = value?.ledger_result_hook || value?.mad_db?.ledger_result_hook || value;
79
97
  const missionId = stringOrNull(candidate?.mission_id || candidate?.missionId);
@@ -107,6 +125,59 @@ function hookMatchesPayload(hook, payload) {
107
125
  return true;
108
126
  return toolText.includes(String(hook.tool_name).toLowerCase()) || String(hook.tool_name).toLowerCase().includes(toolText);
109
127
  }
128
+ function madDbToolUseFailed(payload = {}) {
129
+ if (payload?.isError === true || payload?.tool_response?.isError === true || payload?.toolResponse?.isError === true || payload?.result?.isError === true)
130
+ return true;
131
+ const candidates = [
132
+ payload.exit_code,
133
+ payload.exitCode,
134
+ payload.tool_response?.exit_code,
135
+ payload.toolResponse?.exitCode,
136
+ payload.result?.exit_code,
137
+ payload.result?.exitCode
138
+ ];
139
+ for (const candidate of candidates) {
140
+ if (candidate === undefined || candidate === null || candidate === '')
141
+ continue;
142
+ const n = Number(candidate);
143
+ if (Number.isFinite(n))
144
+ return n !== 0;
145
+ }
146
+ if (payload.success === false || payload.tool_response?.success === false || payload.toolResponse?.success === false || payload.result?.success === false)
147
+ return true;
148
+ if (payload.executed === false)
149
+ return true;
150
+ return false;
151
+ }
152
+ function extractRowCount(payload = {}) {
153
+ const candidates = [
154
+ payload.row_count,
155
+ payload.rowCount,
156
+ payload.tool_response?.row_count,
157
+ payload.tool_response?.rowCount,
158
+ payload.toolResponse?.rowCount,
159
+ payload.result?.row_count,
160
+ payload.result?.rowCount,
161
+ payload.result?.rows_affected,
162
+ payload.tool_response?.rows_affected
163
+ ];
164
+ for (const candidate of candidates) {
165
+ if (candidate === undefined || candidate === null || candidate === '')
166
+ continue;
167
+ const parsed = Number(candidate);
168
+ if (Number.isFinite(parsed))
169
+ return parsed;
170
+ }
171
+ return null;
172
+ }
173
+ function extractToolError(payload = {}) {
174
+ if (payload?.result?.isError === true && Array.isArray(payload.result.content)) {
175
+ const text = payload.result.content.map((entry) => entry?.text || entry?.message || '').filter(Boolean).join('\n');
176
+ if (text.trim())
177
+ return text.trim();
178
+ }
179
+ return String(payload.error || payload.message || payload.stderr || payload.tool_response?.stderr || payload.toolResponse?.stderr || payload.result?.stderr || payload.result?.error || 'tool_failed');
180
+ }
110
181
  async function hasTerminalLifecycleEvent(root, missionId, operationId) {
111
182
  const ledger = path.join(missionDir(root, missionId), 'mad-db-ledger.jsonl');
112
183
  const text = await readText(ledger, '').catch(() => '');
@@ -0,0 +1,29 @@
1
+ import path from 'node:path';
2
+ import { buildCodexPluginInventory } from '../codex-plugins/codex-plugin-json.js';
3
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
4
+ import { policyForPluginMcpServer } from './mcp-server-policy.js';
5
+ export function buildMcpPluginServerCandidates(inventory) {
6
+ const candidates = inventory.plugins.flatMap((plugin) => plugin.remote_mcp_servers.map((server) => policyForPluginMcpServer({
7
+ pluginId: plugin.id,
8
+ name: server.name,
9
+ url: server.url,
10
+ authType: server.auth_type
11
+ })));
12
+ return {
13
+ schema: 'sks.mcp-plugin-server-candidates.v1',
14
+ generated_at: nowIso(),
15
+ candidates,
16
+ candidate_only: true,
17
+ blockers: []
18
+ };
19
+ }
20
+ export async function writeMcpPluginInventoryArtifacts(root, input = {}) {
21
+ const inventory = input.inventory || await buildCodexPluginInventory();
22
+ const candidates = buildMcpPluginServerCandidates(inventory);
23
+ const pluginArtifact = path.join(root, '.sneakoscope', 'codex-plugin-inventory.json');
24
+ const candidateArtifact = path.join(root, '.sneakoscope', 'mcp-plugin-server-candidates.json');
25
+ await writeJsonAtomic(pluginArtifact, inventory);
26
+ await writeJsonAtomic(candidateArtifact, candidates);
27
+ return { inventory, candidates, plugin_artifact: pluginArtifact, candidate_artifact: candidateArtifact };
28
+ }
29
+ //# sourceMappingURL=mcp-plugin-inventory.js.map
@@ -0,0 +1,24 @@
1
+ export function policyForPluginMcpServer(input) {
2
+ const haystack = `${input.name} ${input.url || ''}`.toLowerCase();
3
+ const dbRelated = /supabase|postgres|database|sql|db\b/.test(haystack);
4
+ const oauth = /oauth/i.test(String(input.authType || ''));
5
+ return {
6
+ name: input.name,
7
+ plugin_id: input.pluginId,
8
+ url: input.url || null,
9
+ auth_type: input.authType || null,
10
+ candidate_only: true,
11
+ auto_enable: false,
12
+ destructive_tools_auto_enabled: false,
13
+ db_safety_required: dbRelated,
14
+ mad_db_required_for_destructive: dbRelated,
15
+ oauth_prerefresh_recommended: oauth,
16
+ policy_notes: [
17
+ 'Remote MCP servers from plugin detail are candidate only.',
18
+ 'Do not auto-enable destructive MCP tools.',
19
+ dbRelated ? 'DB MCP servers require DB safety and Mad-DB for destructive operations.' : 'Non-DB MCP candidate still requires explicit operator enablement.',
20
+ oauth ? 'OAuth-backed MCP should trigger pre-refresh doctor check.' : ''
21
+ ].filter(Boolean)
22
+ };
23
+ }
24
+ //# sourceMappingURL=mcp-server-policy.js.map