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.
- package/README.md +19 -28
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/commands/doctor.js +39 -1
- package/dist/core/agents/agent-effort-policy.js +7 -1
- package/dist/core/codex-app/codex-app-handoff.js +77 -0
- package/dist/core/codex-control/codex-0138-capability.js +64 -0
- package/dist/core/codex-control/codex-model-capabilities.js +41 -0
- package/dist/core/codex-control/codex-sdk-config-policy.js +1 -1
- package/dist/core/codex-control/codex-task-runner.js +1 -1
- package/dist/core/codex-plugins/codex-plugin-json.js +152 -0
- package/dist/core/commands/mad-sks-command.js +4 -0
- package/dist/core/commands/naruto-command.js +3 -1
- package/dist/core/commands/qa-loop-command.js +111 -4
- package/dist/core/doctor/codex-0138-doctor.js +104 -0
- package/dist/core/doctor/doctor-readiness-matrix.js +11 -0
- package/dist/core/effort-orchestrator.js +9 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/hooks-runtime.js +6 -9
- package/dist/core/image/image-artifact-path-contract.js +99 -0
- package/dist/core/image-ux-review/imagegen-adapter.js +24 -3
- package/dist/core/mad-db/mad-db-result-lifecycle.js +71 -0
- package/dist/core/mcp/mcp-plugin-inventory.js +29 -0
- package/dist/core/mcp/mcp-server-policy.js +24 -0
- package/dist/core/qa-loop/qa-loop-budget-policy.js +37 -0
- package/dist/core/qa-loop.js +28 -2
- package/dist/core/usage/codex-account-usage.js +78 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-slot-column-anchor.js +16 -7
- package/dist/core/zellij/zellij-slot-pane-renderer.js +18 -0
- package/dist/scripts/release-gate-existence-audit.js +5 -1
- package/package.json +25 -2
- package/schemas/codex-app/codex-app-handoff.schema.json +20 -0
- package/schemas/codex-plugins/codex-plugin-inventory.schema.json +32 -0
- package/schemas/image/image-artifact-path-contract.schema.json +32 -0
- 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.
|
|
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 {
|
|
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
|
-
|
|
454
|
-
if (!hook)
|
|
455
|
-
return null;
|
|
456
|
-
return recordMadDbToolResult({
|
|
455
|
+
return maybeRecordMadDbToolResultFromToolUse({
|
|
457
456
|
root,
|
|
458
457
|
missionId: String(state.mission_id),
|
|
459
|
-
|
|
460
|
-
|
|
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);
|