sneakoscope 2.0.17 → 2.0.18

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 (39) hide show
  1. package/README.md +19 -28
  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/core/agents/agent-effort-policy.js +7 -1
  9. package/dist/core/codex-app/codex-app-handoff.js +77 -0
  10. package/dist/core/codex-control/codex-0138-capability.js +64 -0
  11. package/dist/core/codex-control/codex-model-capabilities.js +41 -0
  12. package/dist/core/codex-control/codex-sdk-config-policy.js +1 -1
  13. package/dist/core/codex-control/codex-task-runner.js +1 -1
  14. package/dist/core/codex-plugins/codex-plugin-json.js +152 -0
  15. package/dist/core/commands/mad-sks-command.js +4 -0
  16. package/dist/core/commands/naruto-command.js +3 -1
  17. package/dist/core/commands/qa-loop-command.js +111 -4
  18. package/dist/core/doctor/codex-0138-doctor.js +104 -0
  19. package/dist/core/doctor/doctor-readiness-matrix.js +11 -0
  20. package/dist/core/effort-orchestrator.js +9 -0
  21. package/dist/core/fsx.js +1 -1
  22. package/dist/core/hooks-runtime.js +6 -9
  23. package/dist/core/image/image-artifact-path-contract.js +99 -0
  24. package/dist/core/image-ux-review/imagegen-adapter.js +24 -3
  25. package/dist/core/mad-db/mad-db-result-lifecycle.js +71 -0
  26. package/dist/core/mcp/mcp-plugin-inventory.js +29 -0
  27. package/dist/core/mcp/mcp-server-policy.js +24 -0
  28. package/dist/core/qa-loop/qa-loop-budget-policy.js +37 -0
  29. package/dist/core/qa-loop.js +28 -2
  30. package/dist/core/usage/codex-account-usage.js +78 -0
  31. package/dist/core/version.js +1 -1
  32. package/dist/core/zellij/zellij-slot-column-anchor.js +16 -7
  33. package/dist/core/zellij/zellij-slot-pane-renderer.js +18 -0
  34. package/dist/scripts/release-gate-existence-audit.js +5 -1
  35. package/package.json +25 -2
  36. package/schemas/codex-app/codex-app-handoff.schema.json +20 -0
  37. package/schemas/codex-plugins/codex-plugin-inventory.schema.json +32 -0
  38. package/schemas/image/image-artifact-path-contract.schema.json +32 -0
  39. package/schemas/usage/codex-account-usage.schema.json +27 -0
@@ -20,6 +20,7 @@ import { planNarutoZellijDashboard } from '../zellij/zellij-naruto-dashboard.js'
20
20
  import { checkPromptPlaceholders } from '../prompt/prompt-placeholder-guard.js';
21
21
  import { evaluateGitWorktreeCapability } from '../git/git-worktree-capability.js';
22
22
  import { buildRuntimeProofSummary, renderRuntimeProofSummary } from '../agents/runtime-proof-summary.js';
23
+ import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
23
24
  const NARUTO_RESULT_SCHEMA = 'sks.naruto-command-result.v1';
24
25
  const NARUTO_ROUTE = '$Naruto';
25
26
  // $Naruto — Shadow Clone Swarm (影分身 / Kage Bunshin no Jutsu).
@@ -73,6 +74,7 @@ async function narutoRun(parsed) {
73
74
  maxAgentCount: MAX_NARUTO_AGENT_COUNT
74
75
  });
75
76
  const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
77
+ await writeCodex0138CapabilityArtifacts(root, { missionId: mission.id }).catch(() => null);
76
78
  const gitWorktreeCapability = writeCapable
77
79
  ? await evaluateGitWorktreeCapability({ root, missionId: mission.id })
78
80
  : null;
@@ -382,7 +384,7 @@ async function narutoRun(parsed) {
382
384
  && Number(parallelRuntime.max_observed_active_workers || 0) >= Math.min(16, activeSlots));
383
385
  await writeJsonAtomic(path.join(mission.dir, 'naruto-gate.json'), {
384
386
  schema: 'sks.naruto-gate.v1',
385
- passed: result.ok === true && nativeProofOk && finalAccepted,
387
+ passed: result.ok === true && nativeProofOk && finalAccepted && parallelRuntimeOk,
386
388
  mission_id: mission.id,
387
389
  clone_roster_built: true,
388
390
  clone_count: roster.agent_count,
@@ -13,6 +13,12 @@ import { scanDbSafety } from '../db-safety.js';
13
13
  import { maybeFinalizeRoute } from '../proof/auto-finalize.js';
14
14
  import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
15
15
  import { flag, promptOf, readBoundedIntegerFlag, readFlagValue, readMaxCycles, resolveMissionId, safeReadTextFile } from './command-utils.js';
16
+ import { runCodexAppHandoff, qaLoopShouldRequestAppHandoff } from '../codex-app/codex-app-handoff.js';
17
+ import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
18
+ import { writeCodexAccountUsageArtifacts } from '../usage/codex-account-usage.js';
19
+ import { buildQaLoopBudgetPolicy, selectQaLoopEscalatedEffort } from '../qa-loop/qa-loop-budget-policy.js';
20
+ import { discoverImageArtifactsInDir, writeImageArtifactPathContract } from '../image/image-artifact-path-contract.js';
21
+ import { pluginAppTemplatePolicy } from '../codex-plugins/codex-plugin-json.js';
16
22
  import fsp from 'node:fs/promises';
17
23
  export async function qaLoopCommand(sub, args = []) {
18
24
  const known = new Set(['prepare', 'answer', 'run', 'status', 'help', '--help', '-h']);
@@ -31,8 +37,8 @@ export async function qaLoopCommand(sub, args = []) {
31
37
  Usage:
32
38
  sks qa-loop prepare "target"
33
39
  sks qa-loop answer <mission-id|latest> <answers.json>
34
- sks qa-loop run <mission-id|latest> [--mock] [--max-cycles N]
35
- sks qa-loop status <mission-id|latest>
40
+ sks qa-loop run <mission-id|latest> [--mock] [--max-cycles N] [--app-handoff] [--app-handoff-required]
41
+ sks qa-loop status <mission-id|latest> [--desktop]
36
42
  `);
37
43
  }
38
44
  function qaRoute() {
@@ -136,6 +142,90 @@ async function qaLoopRun(args) {
136
142
  const qaGate = await readJson(path.join(dir, 'qa-gate.json'), {});
137
143
  const reportFile = qaGate.qa_report_file;
138
144
  const uiRequired = qaUiRequired(contract.answers || {});
145
+ const capabilityArtifact = await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), report: null }));
146
+ const usageArtifact = await writeCodexAccountUsageArtifacts(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), snapshot: null }));
147
+ const budgetPolicy = buildQaLoopBudgetPolicy({ usage: usageArtifact?.snapshot || null, provider: 'codex-sdk' });
148
+ await writeJsonAtomic(path.join(dir, 'qa-loop', 'qa-loop-budget-policy.json'), budgetPolicy);
149
+ const effortEscalation = selectQaLoopEscalatedEffort({
150
+ failureCount: Number(qaGate.safe_fix_attempts || qaGate.failure_count || 0),
151
+ currentEffort: String(profile || 'high').replace(/^sks-(?:logic|agent)-/, '').replace(/-fast$/, '') || 'high'
152
+ });
153
+ await writeJsonAtomic(path.join(dir, 'qa-loop', 'qa-loop-effort-escalation.json'), effortEscalation);
154
+ const discoveredImages = await discoverImageArtifactsInDir(dir).catch(() => []);
155
+ const imagePathContract = discoveredImages.length
156
+ ? await writeQaLoopImagePathContract(root, dir, id, discoveredImages)
157
+ : null;
158
+ const pluginInventory = await readJson(path.join(root, '.sneakoscope', 'codex-plugin-inventory.json'), null);
159
+ const pluginPolicy = pluginInventory?.schema === 'sks.codex-plugin-inventory.v1' ? pluginAppTemplatePolicy(pluginInventory) : null;
160
+ const appHandoffRequired = flag(args, '--app-handoff-required') || process.env.SKS_QA_LOOP_APP_HANDOFF_REQUIRED === '1';
161
+ const appHandoffRequested = qaLoopShouldRequestAppHandoff({
162
+ args,
163
+ uiRequired,
164
+ visualArtifactsPresent: discoveredImages.length > 0,
165
+ pluginAppTemplateUnavailable: Boolean(pluginPolicy?.unavailable_app_templates?.length),
166
+ userRequestedDesktopReview: appHandoffRequired
167
+ });
168
+ const appHandoff = appHandoffRequested || appHandoffRequired
169
+ ? await runCodexAppHandoff(root, {
170
+ schema: 'sks.codex-app-handoff-request.v1',
171
+ mission_id: id,
172
+ route: '$QA-LOOP',
173
+ reason: appHandoffRequired ? 'desktop_app_review_required' : 'desktop_app_review_requested',
174
+ thread_ref: null,
175
+ workspace_path: root,
176
+ artifacts: [
177
+ 'decision-contract.json',
178
+ 'qa-gate.json',
179
+ 'qa-ledger.json',
180
+ reportFile,
181
+ capabilityArtifact && !capabilityArtifact.error ? 'codex-0138-capability.json' : '',
182
+ imagePathContract ? 'qa-loop/image-artifact-path-contract.json' : ''
183
+ ].filter(Boolean),
184
+ prompt: mission.prompt || 'QA-LOOP desktop handoff',
185
+ require_desktop: appHandoffRequired,
186
+ capability_required: 'codex-0.138'
187
+ }).catch((err) => ({
188
+ ok: false,
189
+ status: 'blocked_for_desktop_review',
190
+ artifact_path: path.join(dir, 'qa-loop', 'app-handoff.json'),
191
+ blockers: [`codex_app_handoff_failed:${err?.message || String(err)}`],
192
+ desktop_handoff_supported: false
193
+ }))
194
+ : null;
195
+ if (appHandoff || imagePathContract) {
196
+ const latestGate = await readJson(path.join(dir, 'qa-gate.json'), qaGate);
197
+ const nextGate = {
198
+ ...latestGate,
199
+ desktop_app_handoff_required: appHandoffRequired,
200
+ desktop_app_handoff_status: appHandoff ? appHandoff.status : 'not_requested',
201
+ desktop_app_handoff_artifact: appHandoff ? path.relative(dir, appHandoff.artifact_path) : null,
202
+ desktop_app_handoff_supported: appHandoff ? appHandoff.desktop_handoff_supported === true : false,
203
+ desktop_app_handoff_is_web_ui_evidence: false,
204
+ image_artifact_path_contract_present: Boolean(imagePathContract),
205
+ image_artifact_path_contract_artifact: imagePathContract ? 'qa-loop/image-artifact-path-contract.json' : null,
206
+ image_artifact_path_contract_blockers: imagePathContract?.contract?.blockers || [],
207
+ blockers: Array.from(new Set([
208
+ ...(latestGate.blockers || []),
209
+ ...(appHandoffRequired && appHandoff && appHandoff.ok !== true ? ['blocked_for_desktop_review'] : []),
210
+ ...(imagePathContract?.contract?.blockers || [])
211
+ ])),
212
+ notes: [
213
+ ...(latestGate.notes || []),
214
+ ...(appHandoff ? ['Codex Desktop /app handoff is tracked separately and is not web UI verification evidence.'] : []),
215
+ ...(imagePathContract ? ['Image artifacts expose real saved file paths for follow-up visual edits.'] : [])
216
+ ]
217
+ };
218
+ await writeJsonAtomic(path.join(dir, 'qa-gate.json'), nextGate);
219
+ if (appHandoffRequired && appHandoff && appHandoff.ok !== true) {
220
+ await maybeFinalizeRoute(root, { missionId: id, route: '$QA-LOOP', gateFile: 'qa-gate.json', gate: nextGate, artifacts: ['qa-gate.json', 'qa-ledger.json', reportFile, 'qa-loop/app-handoff.json', 'completion-proof.json'], statusHint: 'blocked', blockers: nextGate.blockers, command: { cmd: `sks qa-loop run ${id} --app-handoff-required`, status: 2 } });
221
+ await setCurrent(root, { mission_id: id, mode: 'QALOOP', phase: 'QALOOP_BLOCKED_DESKTOP_APP_HANDOFF', questions_allowed: true });
222
+ if (flag(args, '--json'))
223
+ return console.log(JSON.stringify({ schema: 'sks.qa-loop-run.v1', ok: false, status: 'blocked_for_desktop_review', mission_id: id, app_handoff: appHandoff, gate: nextGate }, null, 2));
224
+ console.error('QA-LOOP blocked: Codex Desktop /app handoff is required but unavailable or still pending.');
225
+ process.exitCode = 2;
226
+ return;
227
+ }
228
+ }
139
229
  if (uiRequired && !mock) {
140
230
  const chrome = await codexChromeExtensionStatus();
141
231
  if (!chrome.ok) {
@@ -237,7 +327,7 @@ async function qaLoopRun(args) {
237
327
  for (let cycle = 1; cycle <= maxCycles; cycle += 1) {
238
328
  const cycleDir = path.join(dir, 'qa-loop', `cycle-${cycle}`);
239
329
  const outputFile = path.join(cycleDir, 'final.md');
240
- const prompt = buildQaLoopPrompt({ id, mission, contract, cycle, previous: last, reportFile });
330
+ const prompt = buildQaLoopPrompt({ id, mission, contract, cycle, previous: last, reportFile, imagePathContract: imagePathContract?.contract || null, appHandoff });
241
331
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.cycle.start', cycle });
242
332
  const result = await runCodexExec({ root, prompt, outputFile, json: true, profile, logDir: cycleDir });
243
333
  await writeJsonAtomic(path.join(cycleDir, 'process.json'), { code: result.code, stdout_tail: result.stdout, stderr_tail: result.stderr, stdout_bytes: result.stdoutBytes, stderr_bytes: result.stderrBytes, truncated: result.truncated, timed_out: result.timedOut });
@@ -276,8 +366,9 @@ async function qaLoopStatus(args) {
276
366
  const status = await qaStatus(dir);
277
367
  const nativeAgentPlan = await readJson(path.join(dir, 'qa-agent-plan.json'), null);
278
368
  const agentSessions = await readJson(path.join(dir, 'agents', 'agent-sessions.json'), null);
369
+ const desktop = await readJson(path.join(dir, 'qa-loop', 'app-handoff.json'), null);
279
370
  if (flag(args, '--json'))
280
- return console.log(JSON.stringify({ mission, state, qa: status, native_agent_plan: nativeAgentPlan, agent_sessions: agentSessions?.sessions || null }, null, 2));
371
+ return console.log(JSON.stringify({ mission, state, qa: status, desktop_app_handoff: desktop, native_agent_plan: nativeAgentPlan, agent_sessions: agentSessions?.sessions || null }, null, 2));
281
372
  console.log('SKS QA-LOOP Status\n');
282
373
  console.log(`Mission: ${id}`);
283
374
  console.log(`Phase: ${state.phase || mission.phase}`);
@@ -286,5 +377,21 @@ async function qaLoopStatus(args) {
286
377
  console.log(`Gate: ${status.gate?.passed ? 'passed' : 'not passed'}`);
287
378
  if (status.gate?.reasons?.length)
288
379
  console.log(`Reasons: ${status.gate.reasons.join(', ')}`);
380
+ if (flag(args, '--desktop')) {
381
+ console.log('Desktop:');
382
+ console.log(` /app handoff: ${desktop?.status || 'not_requested'}`);
383
+ if (desktop?.operator_instruction?.prompt_artifact)
384
+ console.log(` prompt: ${desktop.operator_instruction.prompt_artifact}`);
385
+ console.log(' web evidence: not a substitute for Codex Chrome Extension web UI verification');
386
+ }
387
+ }
388
+ async function writeQaLoopImagePathContract(root, dir, missionId, images) {
389
+ const primary = await writeImageArtifactPathContract(root, {
390
+ missionId,
391
+ images,
392
+ artifactPath: path.join(dir, 'image-artifact-path-contract.json')
393
+ });
394
+ await writeJsonAtomic(path.join(dir, 'qa-loop', 'image-artifact-path-contract.json'), primary.contract);
395
+ return primary;
289
396
  }
290
397
  //# sourceMappingURL=qa-loop-command.js.map
@@ -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 = '2.0.18';
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,99 @@
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
+ exists,
20
+ mime_type: mimeForPath(filePath),
21
+ width: dims?.width ?? null,
22
+ height: dims?.height ?? null,
23
+ model_visible_path: filePath,
24
+ followup_edit_hint: exists
25
+ ? `Use this saved local path for follow-up image edits: ${filePath}`
26
+ : 'Image file path missing; do not run visual QA until a real saved file path exists.'
27
+ });
28
+ }
29
+ if (images.some((image) => image.kind === 'generated_image' && !image.exists))
30
+ blockers.push('image_generated_file_path_missing');
31
+ return {
32
+ schema: 'sks.image-artifact-path-contract.v1',
33
+ mission_id: input.missionId,
34
+ generated_at: nowIso(),
35
+ images,
36
+ blockers: [...new Set(blockers)]
37
+ };
38
+ }
39
+ export async function writeImageArtifactPathContract(root, input) {
40
+ const contract = await buildImageArtifactPathContract(root, input);
41
+ const artifactPath = input.artifactPath || path.join(root, '.sneakoscope', 'missions', input.missionId, 'image-artifact-path-contract.json');
42
+ await writeJsonAtomic(artifactPath, contract);
43
+ return { contract, artifact_path: artifactPath };
44
+ }
45
+ export async function discoverImageArtifactsInDir(dir) {
46
+ const out = [];
47
+ await walk(dir, async (file) => {
48
+ if (!/\.(png|jpe?g|webp|gif)$/i.test(file))
49
+ return;
50
+ out.push({
51
+ id: path.basename(file).replace(/[^0-9A-Za-z._-]/g, '_'),
52
+ kind: /generated|gpt-image|callout/i.test(file) ? 'generated_image' : 'visual_qa_snapshot',
53
+ filePath: file
54
+ });
55
+ });
56
+ return out;
57
+ }
58
+ function mimeForPath(file) {
59
+ const ext = path.extname(file).toLowerCase();
60
+ if (ext === '.png')
61
+ return 'image/png';
62
+ if (ext === '.jpg' || ext === '.jpeg')
63
+ return 'image/jpeg';
64
+ if (ext === '.webp')
65
+ return 'image/webp';
66
+ if (ext === '.gif')
67
+ return 'image/gif';
68
+ return null;
69
+ }
70
+ async function fileExists(file) {
71
+ try {
72
+ const st = await fs.stat(file);
73
+ return st.isFile();
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ async function walk(dir, visit) {
80
+ let entries;
81
+ try {
82
+ entries = await fs.readdir(dir, { withFileTypes: true });
83
+ }
84
+ catch {
85
+ return;
86
+ }
87
+ for (const entry of entries) {
88
+ const full = path.join(dir, entry.name);
89
+ if (entry.isDirectory()) {
90
+ if (['node_modules', '.git', 'dist'].includes(entry.name))
91
+ continue;
92
+ await walk(full, visit);
93
+ }
94
+ else {
95
+ await visit(full);
96
+ }
97
+ }
98
+ }
99
+ //# sourceMappingURL=image-artifact-path-contract.js.map
@@ -7,6 +7,7 @@ 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';
10
11
  const DEFAULT_OPENAI_IMAGE_EDITS_ENDPOINT = 'https://api.openai.com/v1/images/edits';
11
12
  export function buildCalloutPrompt(sourceScreenId, context = {}) {
12
13
  return [
@@ -124,6 +125,7 @@ export function createCodexAppImagegenAdapter(opts = {}) {
124
125
  real_generated: true
125
126
  });
126
127
  const outputSource = manualOutput ? 'manual_attach' : 'auto_discovered_generated_images';
128
+ const imageContract = await writeGeneratedImagePathContract(input, dest, 'codex_app_imagegen').catch(() => null);
127
129
  await writeJsonAtomic(responseArtifact, {
128
130
  schema: 'sks.image-ux-gpt-image-2-response.v1',
129
131
  created_at: nowIso(),
@@ -135,6 +137,7 @@ export function createCodexAppImagegenAdapter(opts = {}) {
135
137
  output_image_sha256: meta.sha256,
136
138
  output_id: meta.output_id,
137
139
  output_source: outputSource,
140
+ image_artifact_path_contract: imageContract?.artifact_path || null,
138
141
  discovered_from: discovery?.selected?.path || null,
139
142
  discovery: discovery ? { candidates_considered: discovery.candidates_considered, since_ms: discovery.since_ms, max_age_ms: discovery.max_age_ms } : null,
140
143
  local_only: true
@@ -149,6 +152,7 @@ export function createCodexAppImagegenAdapter(opts = {}) {
149
152
  output_source: outputSource,
150
153
  request_artifact: requestArtifact,
151
154
  response_artifact: responseArtifact,
155
+ image_artifact_path_contract: imageContract?.artifact_path || null,
152
156
  latency_ms: null
153
157
  };
154
158
  }
@@ -241,6 +245,7 @@ export function createFakeImagegenAdapter(opts = {}) {
241
245
  real_generated: false,
242
246
  mock: true
243
247
  });
248
+ const imageContract = await writeGeneratedImagePathContract(input, out, 'fake_imagegen_adapter').catch(() => null);
244
249
  await writeJsonAtomic(responseArtifact, {
245
250
  schema: 'sks.image-ux-gpt-image-2-response.v1',
246
251
  created_at: nowIso(),
@@ -251,6 +256,7 @@ export function createFakeImagegenAdapter(opts = {}) {
251
256
  output_image_path: out,
252
257
  output_image_sha256: meta.sha256,
253
258
  output_id: meta.output_id,
259
+ image_artifact_path_contract: imageContract?.artifact_path || null,
254
260
  dimensions: { width: meta.width, height: meta.height, format: meta.format },
255
261
  latency_ms: Date.now() - started,
256
262
  fake_adapter: true,
@@ -259,7 +265,7 @@ export function createFakeImagegenAdapter(opts = {}) {
259
265
  mock: true,
260
266
  local_only: true
261
267
  });
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 };
268
+ 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
269
  }
264
270
  };
265
271
  }
@@ -385,6 +391,7 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
385
391
  output_id: generated.id || payload?.id || null,
386
392
  real_generated: true
387
393
  });
394
+ const imageContract = await writeGeneratedImagePathContract(input, out, 'openai_responses_image_generation').catch(() => null);
388
395
  await writeJsonAtomic(responseArtifact, {
389
396
  schema: 'sks.image-ux-gpt-image-2-response.v1',
390
397
  created_at: nowIso(),
@@ -397,12 +404,13 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
397
404
  output_image_path: out,
398
405
  output_image_sha256: meta.sha256,
399
406
  output_id: meta.output_id,
407
+ image_artifact_path_contract: imageContract?.artifact_path || null,
400
408
  dimensions: { width: meta.width, height: meta.height, format: meta.format },
401
409
  latency_ms: Date.now() - started,
402
410
  token_cost_metadata: payload?.usage || null,
403
411
  local_only: true
404
412
  });
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 };
413
+ 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
414
  }
407
415
  const sourceBytes = await fsp.readFile(sourcePath);
408
416
  const qualityParam = imagegenQualityParam(opts);
@@ -440,6 +448,7 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
440
448
  output_id: image?.id || payload?.id || null,
441
449
  real_generated: true
442
450
  });
451
+ const imageContract = await writeGeneratedImagePathContract(input, out, 'openai_images_api').catch(() => null);
443
452
  await writeJsonAtomic(responseArtifact, {
444
453
  schema: 'sks.image-ux-gpt-image-2-response.v1',
445
454
  created_at: nowIso(),
@@ -451,12 +460,13 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
451
460
  output_image_path: out,
452
461
  output_image_sha256: meta.sha256,
453
462
  output_id: meta.output_id,
463
+ image_artifact_path_contract: imageContract?.artifact_path || null,
454
464
  dimensions: { width: meta.width, height: meta.height, format: meta.format },
455
465
  latency_ms: Date.now() - started,
456
466
  token_cost_metadata: payload?.usage || null,
457
467
  local_only: true
458
468
  });
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 };
469
+ 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
470
  }
461
471
  catch (err) {
462
472
  const provider = useResponsesImageTool ? 'openai_responses_image_generation' : 'openai_images_api';
@@ -468,6 +478,17 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
468
478
  }
469
479
  };
470
480
  }
481
+ async function writeGeneratedImagePathContract(input, outputPath, provider) {
482
+ return writeImageArtifactPathContract(process.cwd(), {
483
+ missionId: input.mission_id || 'unassigned',
484
+ images: [{
485
+ id: `${provider}-${input.source_screen_id || 'screen'}`,
486
+ kind: 'generated_image',
487
+ filePath: outputPath
488
+ }],
489
+ artifactPath: path.join(input.output_dir, 'image-artifact-path-contract.json')
490
+ });
491
+ }
471
492
  export async function generateGptImage2CalloutReview(input, opts = {}) {
472
493
  if (opts.fake === true || process.env.SKS_TEST_FAKE_IMAGEGEN === '1') {
473
494
  return createFakeImagegenAdapter(opts.fakeAdapter || {}).generateCalloutReview(input);