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
@@ -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
@@ -0,0 +1,37 @@
1
+ import { defaultModelCallBudget } from '../codex-control/model-call-concurrency.js';
2
+ import { codexModelEffortCapability, nextAdvertisedEffort } from '../codex-control/codex-model-capabilities.js';
3
+ export function buildQaLoopBudgetPolicy(input = {}) {
4
+ const usage = input.usage || null;
5
+ const available = Boolean(usage?.token_usage);
6
+ const limit = Number(usage?.usage_limit_tokens || 0);
7
+ const total = Number(usage?.token_usage?.total_tokens || 0);
8
+ const nearLimit = Boolean(limit > 0 && total / limit >= 0.9);
9
+ const baseBudget = defaultModelCallBudget(String(input.provider || 'codex-sdk'));
10
+ return {
11
+ schema: 'sks.qa-loop-budget-policy.v1',
12
+ ok: true,
13
+ account_usage_source: usage?.source || 'unavailable',
14
+ token_usage_available: available,
15
+ near_limit: nearLimit,
16
+ remote_model_call_concurrency: nearLimit ? Math.max(1, Math.min(2, baseBudget)) : baseBudget,
17
+ local_llm_draft_preferred: nearLimit,
18
+ final_reviewer_gpt_backed: true,
19
+ warnings: available ? [] : ['codex_account_usage_unavailable_no_hard_block']
20
+ };
21
+ }
22
+ export function selectQaLoopEscalatedEffort(input = {}) {
23
+ const capability = input.capability || codexModelEffortCapability();
24
+ const current = input.currentEffort || capability.default_effort;
25
+ const failureCount = Number(input.failureCount || 0);
26
+ return {
27
+ schema: 'sks.qa-loop-effort-escalation.v1',
28
+ model: capability.model,
29
+ advertised_efforts: capability.advertised_efforts,
30
+ order_source: capability.order_source,
31
+ failure_count: failureCount,
32
+ current_effort: current,
33
+ next_effort: failureCount >= 2 ? nextAdvertisedEffort(current, capability) : current,
34
+ escalated: failureCount >= 2 && nextAdvertisedEffort(current, capability) !== current
35
+ };
36
+ }
37
+ //# sourceMappingURL=qa-loop-budget-policy.js.map
@@ -309,6 +309,14 @@ export function defaultQaGate(contract = {}, opts = {}) {
309
309
  ui_chrome_extension_evidence: !uiRequired,
310
310
  ui_computer_use_evidence: false,
311
311
  ui_evidence_source: uiRequired ? null : 'not_required',
312
+ desktop_app_handoff_required: false,
313
+ desktop_app_handoff_status: 'not_requested',
314
+ desktop_app_handoff_artifact: null,
315
+ desktop_app_handoff_supported: false,
316
+ desktop_app_handoff_is_web_ui_evidence: false,
317
+ image_artifact_path_contract_present: false,
318
+ image_artifact_path_contract_artifact: null,
319
+ image_artifact_path_contract_blockers: [],
312
320
  api_e2e_required: apiRequired,
313
321
  unsafe_external_side_effects: false,
314
322
  corrective_loop_enabled: corrective,
@@ -374,6 +382,15 @@ export async function evaluateQaGate(dir) {
374
382
  if (evidenceMentionsForbiddenWebComputerUseEvidence({ evidence: gate.evidence, ui_evidence_source: gate.ui_evidence_source }))
375
383
  reasons.push('computer_use_web_evidence_forbidden');
376
384
  }
385
+ if (gate.desktop_app_handoff_required === true) {
386
+ if (gate.desktop_app_handoff_status !== 'pending' && gate.desktop_app_handoff_status !== 'completed')
387
+ reasons.push('desktop_app_handoff_missing');
388
+ if (gate.desktop_app_handoff_is_web_ui_evidence === true)
389
+ reasons.push('desktop_app_handoff_misused_as_web_evidence');
390
+ }
391
+ const imageBlockers = Array.isArray(gate.image_artifact_path_contract_blockers) ? gate.image_artifact_path_contract_blockers : [];
392
+ if (imageBlockers.includes('image_generated_file_path_missing'))
393
+ reasons.push('image_generated_file_path_missing');
377
394
  if (!reportFile)
378
395
  reasons.push('qa_report_file_missing');
379
396
  else if (!isQaReportFilename(reportFile))
@@ -396,8 +413,14 @@ export async function writeMockQaResult(dir, mission, contract) {
396
413
  await writeJsonAtomic(path.join(dir, 'qa-gate.json'), { ...defaultQaGate(contract, { reportFile }), passed: !uiRequired, qa_report_written: true, qa_ledger_complete: true, checklist_completed: true, safety_reviewed: true, credentials_not_persisted: true, chrome_extension_preflight_passed: !uiRequired, ui_chrome_extension_evidence: !uiRequired, ui_computer_use_evidence: false, ui_evidence_source: uiRequired ? null : 'not_required', unresolved_findings: 0, unresolved_fixable_findings: 0, unsafe_or_deferred_findings: 0, post_fix_verification_complete: true, honest_mode_complete: true, evidence: ['mock QA-LOOP smoke completed'], notes: ['No live UI/API verification was claimed.'] });
397
414
  return evaluateQaGate(dir);
398
415
  }
399
- export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile }) {
416
+ export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile, imagePathContract, appHandoff }) {
400
417
  const report = reportFile && isQaReportFilename(reportFile) ? reportFile : 'the date/version-prefixed report named by qa-gate.json.qa_report_file';
418
+ const imageContractText = imagePathContract
419
+ ? `\nIMAGE PATH CONTRACT:\n${JSON.stringify(imagePathContract, null, 2)}\nUse model_visible_path values for follow-up image edits; do not invent generated image paths.\n`
420
+ : '';
421
+ const appHandoffText = appHandoff
422
+ ? `\nCODEX DESKTOP /app HANDOFF:\n${JSON.stringify(appHandoff, null, 2)}\nThis is desktop-app review status only and is not web UI evidence.\n`
423
+ : '';
401
424
  return `SKS QA-LOOP
402
425
  MISSION: ${id}
403
426
  TASK: ${mission.prompt}
@@ -410,6 +433,7 @@ GATE: passed=false while unresolved_findings or unresolved_fixable_findings > 0,
410
433
  ARTIFACTS: update qa-ledger.json, ${report}, qa-gate.json, and qa-loop/cycle-${cycle}/.
411
434
  CONTRACT:
412
435
  ${JSON.stringify(contract, null, 2)}
436
+ ${imageContractText}${appHandoffText}
413
437
  Previous tail:
414
438
  ${String(previous || '').slice(-2500)}
415
439
  `;
@@ -417,9 +441,11 @@ ${String(previous || '').slice(-2500)}
417
441
  export async function qaStatus(dir) {
418
442
  const gate = await readJson(path.join(dir, 'qa-gate.evaluated.json'), await readJson(path.join(dir, 'qa-gate.json'), null));
419
443
  const ledger = await readJson(path.join(dir, 'qa-ledger.json'), null);
444
+ const appHandoff = await readJson(path.join(dir, 'qa-loop', 'app-handoff.json'), null);
445
+ const imagePathContract = await readJson(path.join(dir, 'qa-loop', 'image-artifact-path-contract.json'), null);
420
446
  const reportFile = qaReportFileFromGate(gate?.gate || gate || {}) || ledger?.qa_report_file || null;
421
447
  const report = reportFile && isQaReportFilename(reportFile) ? await readText(path.join(dir, reportFile), '') : '';
422
- return { gate, checklist_count: ledger?.checklist?.length ?? null, report_file: reportFile, report_written: Boolean(report.trim()) };
448
+ return { gate, checklist_count: ledger?.checklist?.length ?? null, report_file: reportFile, report_written: Boolean(report.trim()), desktop_app_handoff: appHandoff, image_path_contract: imagePathContract };
423
449
  }
424
450
  function qaChecklist(a) {
425
451
  const cases = [
@@ -0,0 +1,78 @@
1
+ import path from 'node:path';
2
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
3
+ export async function collectCodexAccountUsage() {
4
+ if (process.env.SKS_CODEX_ACCOUNT_USAGE_FAKE === '1') {
5
+ return {
6
+ schema: 'sks.codex-account-usage.v1',
7
+ generated_at: nowIso(),
8
+ ok: true,
9
+ source: 'fake',
10
+ account_id: 'fake-account',
11
+ token_usage: {
12
+ input_tokens: 1000,
13
+ output_tokens: 500,
14
+ total_tokens: 1500,
15
+ reset_at: null
16
+ },
17
+ usage_limit_tokens: 100000,
18
+ blockers: []
19
+ };
20
+ }
21
+ const url = String(process.env.SKS_CODEX_APP_SERVER_USAGE_URL || process.env.CODEX_APP_SERVER_USAGE_URL || '').trim();
22
+ if (!url)
23
+ return unavailable(['codex_app_server_usage_endpoint_unavailable']);
24
+ try {
25
+ const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
26
+ if (!response.ok)
27
+ return unavailable([`codex_app_server_usage_http_${response.status}`]);
28
+ const payload = await response.json();
29
+ return normalizeUsagePayload(payload, 'app-server');
30
+ }
31
+ catch (err) {
32
+ return unavailable([`codex_app_server_usage_fetch_failed:${err?.message || String(err)}`]);
33
+ }
34
+ }
35
+ export async function writeCodexAccountUsageArtifacts(root, input = {}) {
36
+ const snapshot = await collectCodexAccountUsage();
37
+ const rootArtifact = path.join(root, '.sneakoscope', 'codex-account-usage.json');
38
+ await writeJsonAtomic(rootArtifact, snapshot);
39
+ let missionArtifact = null;
40
+ if (input.missionId) {
41
+ missionArtifact = path.join(root, '.sneakoscope', 'missions', input.missionId, 'codex-account-usage.json');
42
+ await writeJsonAtomic(missionArtifact, snapshot);
43
+ }
44
+ return { snapshot, root_artifact: rootArtifact, mission_artifact: missionArtifact };
45
+ }
46
+ function normalizeUsagePayload(payload, source) {
47
+ const usage = payload?.token_usage || payload?.usage || payload;
48
+ const input = Number(usage?.input_tokens || usage?.inputTokens || 0);
49
+ const output = Number(usage?.output_tokens || usage?.outputTokens || 0);
50
+ const total = Number(usage?.total_tokens || usage?.totalTokens || input + output);
51
+ return {
52
+ schema: 'sks.codex-account-usage.v1',
53
+ generated_at: nowIso(),
54
+ ok: true,
55
+ source,
56
+ account_id: payload?.account_id || payload?.accountId || null,
57
+ token_usage: {
58
+ input_tokens: Number.isFinite(input) ? input : 0,
59
+ output_tokens: Number.isFinite(output) ? output : 0,
60
+ total_tokens: Number.isFinite(total) ? total : 0,
61
+ reset_at: usage?.reset_at || usage?.resetAt || null
62
+ },
63
+ usage_limit_tokens: Number.isFinite(Number(payload?.usage_limit_tokens || payload?.usageLimitTokens)) ? Number(payload?.usage_limit_tokens || payload?.usageLimitTokens) : null,
64
+ blockers: []
65
+ };
66
+ }
67
+ function unavailable(blockers) {
68
+ return {
69
+ schema: 'sks.codex-account-usage.v1',
70
+ generated_at: nowIso(),
71
+ ok: true,
72
+ source: 'unavailable',
73
+ token_usage: null,
74
+ usage_limit_tokens: null,
75
+ blockers
76
+ };
77
+ }
78
+ //# sourceMappingURL=codex-account-usage.js.map
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '2.0.17';
1
+ export const PACKAGE_VERSION = '2.0.18';
2
2
  //# sourceMappingURL=version.js.map
@@ -10,18 +10,21 @@ export function renderZellijSlotColumnAnchor(input = {}) {
10
10
  const fail = nonNegativeInt(input.failedWorkers, 0);
11
11
  const update = input.updateAvailableVersion ? ` · update ${trimInline(input.updateAvailableVersion, 18)} available` : '';
12
12
  const madDb = input.madDbActive ? ' · MAD-DB ACTIVE' : '';
13
+ const appHandoff = input.qaAppHandoffPending ? ' · QA /app handoff pending' : '';
13
14
  const header = done || fail
14
- ? `SLOTS active ${active} · headless ${headless} · done ${done} · fail ${fail} · q ${queue}${update}${madDb}`
15
- : `SLOTS active ${active}/${visible} · headless ${headless} · q ${queue}${update}${madDb}`;
15
+ ? `SLOTS active ${active} · headless ${headless} · done ${done} · fail ${fail} · q ${queue}${update}${madDb}${appHandoff}`
16
+ : `SLOTS active ${active}/${visible} · headless ${headless} · q ${queue}${update}${madDb}${appHandoff}`;
16
17
  const workers = Array.isArray(input.workerRows) ? input.workerRows : [];
18
+ const handoffLine = input.qaAppHandoffPending ? `QA app handoff pending · ${trimInline(input.qaAppHandoffArtifact || 'qa-loop/app-handoff.json', 64)}` : null;
17
19
  if (!workers.length)
18
- return `${header}\nvisible slot panes stack below this anchor`;
20
+ return [header, handoffLine, 'visible slot panes stack below this anchor'].filter(Boolean).join('\n');
19
21
  const maxRows = Math.max(1, nonNegativeInt(input.maxWorkerRows, input.mode === 'full-debug' ? 24 : 12));
20
22
  const overflowRows = workers.filter((row) => row.placement === 'headless').slice(0, maxRows);
21
23
  const visibleRows = overflowRows.length ? overflowRows : workers.filter((row) => row.placement !== 'zellij-pane').slice(0, maxRows);
22
24
  const hidden = Math.max(0, workers.length - visibleRows.length);
23
25
  return [
24
26
  header,
27
+ handoffLine,
25
28
  `visible slot panes stack below this anchor`,
26
29
  ...visibleRows.map((row, index) => renderWorkerRow(row, index + 1)),
27
30
  ...(hidden && visibleRows.length ? [`+${hidden} worker${hidden === 1 ? '' : 's'} in dedicated panes or overflow`] : [])
@@ -33,8 +36,9 @@ export async function renderZellijSlotColumnAnchorFromArtifacts(input) {
33
36
  const telemetry = await readZellijSlotTelemetrySnapshot(root, input.missionId).catch(() => null);
34
37
  const updateNotice = await readJson(path.join(missionDir, 'update-notice.json'));
35
38
  const madDb = await readJson(path.join(missionDir, 'mad-db-capability.json'));
39
+ const appHandoff = await readJson(path.join(missionDir, 'qa-loop', 'app-handoff.json'));
36
40
  if (telemetry && Object.keys(telemetry.slots || {}).length) {
37
- return renderTelemetryAnchor(telemetry, updateNotice, madDb);
41
+ return renderTelemetryAnchor(telemetry, updateNotice, madDb, appHandoff);
38
42
  }
39
43
  const snapshot = await readJson(path.join(missionDir, 'zellij-dashboard-snapshot.json'));
40
44
  const rightColumn = await readJson(path.join(missionDir, 'zellij-right-column-state.json'));
@@ -50,20 +54,25 @@ export async function renderZellijSlotColumnAnchorFromArtifacts(input) {
50
54
  anchorInput.updateAvailableVersion = String(updateNotice.latest_version);
51
55
  if (isMadDbActive(madDb))
52
56
  anchorInput.madDbActive = true;
57
+ if (['pending', 'blocked_for_desktop_review'].includes(String(appHandoff?.status || ''))) {
58
+ anchorInput.qaAppHandoffPending = true;
59
+ anchorInput.qaAppHandoffArtifact = appHandoff?.artifact_path || 'qa-loop/app-handoff.json';
60
+ }
53
61
  if (input.mode !== undefined)
54
62
  anchorInput.mode = input.mode;
55
63
  return renderZellijSlotColumnAnchor(anchorInput);
56
64
  }
57
- function renderTelemetryAnchor(snapshot, updateNotice = null, madDbCapability = null) {
65
+ function renderTelemetryAnchor(snapshot, updateNotice = null, madDbCapability = null, appHandoff = null) {
58
66
  const updatedAt = Date.parse(snapshot.updated_at || '');
59
67
  const staleSeconds = Number.isFinite(updatedAt) ? Math.max(0, Math.round((Date.now() - updatedAt) / 1000)) : null;
60
68
  const counts = snapshot.counts || {};
61
69
  const active = Number(counts.running || 0) + Number(counts.verifying || 0);
62
70
  const update = updateNotice?.update_available && updateNotice?.latest_version ? ` · update ${trimInline(String(updateNotice.latest_version), 18)} available` : '';
63
71
  const madDb = isMadDbActive(madDbCapability) ? ' · MAD-DB ACTIVE' : '';
72
+ const qaHandoff = ['pending', 'blocked_for_desktop_review'].includes(String(appHandoff?.status || '')) ? ' · QA /app handoff pending' : '';
64
73
  if (staleSeconds != null && staleSeconds > 10)
65
- return `SLOTS telemetry stale ${staleSeconds}s · active ?${update}${madDb}`;
66
- return `SLOTS active ${active} · headless ${Number(counts.headless || 0)} · done ${Number(counts.completed || 0)} · fail ${Number(counts.failed || 0)} · q ${Number(counts.queued || 0)}${update}${madDb}`;
74
+ return `SLOTS telemetry stale ${staleSeconds}s · active ?${update}${madDb}${qaHandoff}`;
75
+ return `SLOTS active ${active} · headless ${Number(counts.headless || 0)} · done ${Number(counts.completed || 0)} · fail ${Number(counts.failed || 0)} · q ${Number(counts.queued || 0)}${update}${madDb}${qaHandoff}`;
67
76
  }
68
77
  function isMadDbActive(capability) {
69
78
  if (!capability || capability.enabled !== true || capability.consumed === true)
@@ -24,6 +24,7 @@ export function renderZellijSlotPane(input) {
24
24
  `doing: ${task}`,
25
25
  `files: ${trimInline(files.length ? files.join(', ') : 'no changed file yet', 78)}`,
26
26
  `patch: ${trimInline(input.patchStatus || 'queued', 24)} verify: ${trimInline(input.verifyStatus || 'queued', 24)}`,
27
+ input.qaAppHandoffPending ? `QA app handoff pending: ${trimInline(input.qaAppHandoffArtifact || 'qa-loop/app-handoff.json', 55)}` : null,
27
28
  ...events.map((event) => `event: ${trimInline(event, 78)}`),
28
29
  ...stdout.map((line) => `out: ${trimInline(line, 79)}`),
29
30
  ...stderr.map((line) => `err: ${trimInline(line, 79)}`)
@@ -97,6 +98,7 @@ async function renderZellijSlotPaneFromArtifactDir(input) {
97
98
  ...(Array.isArray(intake?.input_files) ? intake.input_files : [])
98
99
  ]);
99
100
  const now = Date.now();
101
+ const qaAppHandoff = await readQaAppHandoffNearArtifactDir(artifactDir);
100
102
  if (!result && !intake && !backendReport && !fastReport && !paneReport && !codexProof && !localProof && !heartbeatMtime && !eventRows.length)
101
103
  return null;
102
104
  return renderZellijSlotPane({
@@ -155,9 +157,25 @@ async function renderZellijSlotPaneFromArtifactDir(input) {
155
157
  eventLines: eventRows.map(formatArtifactEvent).filter(Boolean),
156
158
  stdoutTail: await readTextTailLines(path.join(artifactDir, 'worker.stdout.log'), 2),
157
159
  stderrTail: await readTextTailLines(path.join(artifactDir, 'worker.stderr.log'), 1),
160
+ qaAppHandoffPending: ['pending', 'blocked_for_desktop_review'].includes(String(qaAppHandoff?.status || '')),
161
+ qaAppHandoffArtifact: qaAppHandoff?.artifact_path || null,
158
162
  mode: input.mode || 'compact-slots'
159
163
  });
160
164
  }
165
+ async function readQaAppHandoffNearArtifactDir(artifactDir) {
166
+ let current = path.resolve(artifactDir);
167
+ for (let i = 0; i < 8; i += 1) {
168
+ const candidate = path.join(current, 'qa-loop', 'app-handoff.json');
169
+ const json = await readJson(candidate);
170
+ if (json)
171
+ return json;
172
+ const next = path.dirname(current);
173
+ if (next === current)
174
+ break;
175
+ current = next;
176
+ }
177
+ return null;
178
+ }
161
179
  export async function renderZellijSlotPaneStatusFromArtifacts(input) {
162
180
  const snapshot = input.missionId && input.missionId !== 'latest'
163
181
  ? await readZellijSlotTelemetrySnapshot(path.resolve(input.artifactRoot || input.artifactDir), input.missionId).catch(() => null)
@@ -84,7 +84,11 @@ const required = [
84
84
  'git-collaboration:e2e'
85
85
  ];
86
86
  assertGate(releaseManifest.schema === 'sks.release-gates.v2', 'release gate manifest schema mismatch', { schema: releaseManifest.schema });
87
- assertGate(String(scripts['release:check'] || '').includes('release-gate-dag-runner') && String(scripts['release:check'] || '').includes('--preset release'), 'release:check must use the v2 DAG release preset', { release_check: scripts['release:check'] });
87
+ const releaseCheck = String(scripts['release:check'] || '');
88
+ const releaseCheckTarget = releaseCheck.includes('release:check:affected')
89
+ ? String(scripts['release:check:affected'] || '')
90
+ : releaseCheck;
91
+ assertGate(releaseCheckTarget.includes('release-gate-dag-runner') && /--preset\s+(?:release|affected)/.test(releaseCheckTarget), 'release:check must use the v2 DAG release/affected preset', { release_check: scripts['release:check'], resolved_release_check: releaseCheckTarget });
88
92
  assertGate(releaseGates.length > 0, 'release v2 manifest must include release preset gates', { gate_count: releaseGates.length });
89
93
  for (const name of required) {
90
94
  assertGate(Boolean(scripts[name]), `missing release gate script: ${name}`, { required });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "2.0.17",
4
+ "version": "2.0.18",
5
5
  "description": "Sneakoscope Codex: fast proof-first Codex trust layer with image-based Voxel TriWiki.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -557,6 +557,29 @@
557
557
  "release:check:dag:explain": "node ./dist/scripts/release-gate-dag-runner.js --preset release --explain",
558
558
  "release:check:dag:no-cache": "node ./dist/scripts/release-gate-dag-runner.js --preset release --no-cache",
559
559
  "release:check:dag:fail-fast": "node ./dist/scripts/release-gate-dag-runner.js --preset release --fail-fast",
560
+ "codex:0138-capability": "node ./dist/scripts/codex-0138-capability-check.js",
561
+ "codex:0138-capability-artifact": "node ./dist/scripts/codex-0138-capability-artifact-check.js",
562
+ "codex-sdk:version-compat": "node ./dist/scripts/codex-sdk-version-compat-check.js",
563
+ "codex-app:handoff": "node ./dist/scripts/codex-app-handoff-check.js",
564
+ "qa-loop:app-handoff": "node ./dist/scripts/qa-loop-app-handoff-check.js",
565
+ "qa-loop:app-handoff-capability": "node ./dist/scripts/qa-loop-app-handoff-capability-check.js",
566
+ "qa-loop:app-handoff-cli": "node ./dist/scripts/qa-loop-app-handoff-cli-check.js",
567
+ "zellij:qa-app-handoff-status": "node ./dist/scripts/zellij-qa-app-handoff-status-check.js",
568
+ "codex-plugin:json": "node ./dist/scripts/codex-plugin-json-check.js",
569
+ "codex-plugin:inventory": "node ./dist/scripts/codex-plugin-inventory-check.js",
570
+ "mcp:plugin-inventory": "node ./dist/scripts/mcp-plugin-inventory-check.js",
571
+ "codex-plugin:app-template-policy": "node ./dist/scripts/codex-plugin-app-template-policy-check.js",
572
+ "image:artifact-path-contract": "node ./dist/scripts/image-artifact-path-contract-check.js",
573
+ "qa-loop:image-path-exposure": "node ./dist/scripts/qa-loop-image-path-exposure-check.js",
574
+ "image:generation-path-handoff": "node ./dist/scripts/image-generation-path-handoff-check.js",
575
+ "image:followup-edit-path": "node ./dist/scripts/image-followup-edit-path-check.js",
576
+ "codex:effort-order": "node ./dist/scripts/codex-effort-order-check.js",
577
+ "qa-loop:effort-escalation": "node ./dist/scripts/qa-loop-effort-escalation-check.js",
578
+ "codex:account-usage": "node ./dist/scripts/codex-account-usage-check.js",
579
+ "qa-loop:budget-policy": "node ./dist/scripts/qa-loop-budget-policy-check.js",
580
+ "naruto:parallel-gate-consistency": "node ./dist/scripts/naruto-parallel-gate-consistency-check.js",
581
+ "codex:0138-doctor": "node ./dist/scripts/codex-0138-doctor-check.js",
582
+ "doctor:codex-0138-fix": "node ./dist/scripts/doctor-codex-0138-fix-check.js",
560
583
  "release:dag-runner": "node ./dist/scripts/release-gate-dag-runner-check.js",
561
584
  "release:parallel-speed-budget": "node ./dist/scripts/release-parallel-speed-budget-check.js",
562
585
  "release:stability-report": "node ./dist/scripts/release-stability-report-check.js",
@@ -711,7 +734,7 @@
711
734
  "license": "MIT",
712
735
  "dependencies": {
713
736
  "@modelcontextprotocol/sdk": "1.29.0",
714
- "@openai/codex-sdk": "0.137.0",
737
+ "@openai/codex-sdk": "^0.138.0",
715
738
  "figlet": "^1.11.0",
716
739
  "typescript": "^5.9.3"
717
740
  },
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://sneakoscope.dev/schemas/codex-app/codex-app-handoff.schema.json",
4
+ "title": "SKS Codex App Handoff",
5
+ "type": "object",
6
+ "required": ["schema", "ok", "status", "desktop_handoff_supported", "artifact_path", "prompt_artifact_path"],
7
+ "properties": {
8
+ "schema": { "const": "sks.codex-app-handoff-result.v1" },
9
+ "ok": { "type": "boolean" },
10
+ "attempted": { "type": "boolean" },
11
+ "launched": { "type": "boolean" },
12
+ "status": { "enum": ["pending", "skipped", "blocked_for_desktop_review"] },
13
+ "desktop_handoff_supported": { "type": "boolean" },
14
+ "artifact_path": { "type": "string", "minLength": 1 },
15
+ "prompt_artifact_path": { "type": "string", "minLength": 1 },
16
+ "blockers": { "type": "array", "items": { "type": "string" } },
17
+ "request": { "type": "object" }
18
+ },
19
+ "additionalProperties": true
20
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://sneakoscope.dev/schemas/codex-plugins/codex-plugin-inventory.schema.json",
4
+ "title": "SKS Codex Plugin Inventory",
5
+ "type": "object",
6
+ "required": ["schema", "generated_at", "plugins", "marketplace_available", "blockers"],
7
+ "properties": {
8
+ "schema": { "const": "sks.codex-plugin-inventory.v1" },
9
+ "generated_at": { "type": "string" },
10
+ "marketplace_available": { "type": "boolean" },
11
+ "plugins": {
12
+ "type": "array",
13
+ "items": {
14
+ "type": "object",
15
+ "required": ["id", "name", "source", "installed", "enabled", "default_prompts", "remote_mcp_servers", "unavailable_app_templates"],
16
+ "properties": {
17
+ "id": { "type": "string", "minLength": 1 },
18
+ "name": { "type": "string" },
19
+ "source": { "enum": ["marketplace", "local", "remote", "unknown"] },
20
+ "installed": { "type": "boolean" },
21
+ "enabled": { "type": "boolean" },
22
+ "default_prompts": { "type": "array", "items": { "type": "string" } },
23
+ "remote_mcp_servers": { "type": "array", "items": { "type": "object" } },
24
+ "unavailable_app_templates": { "type": "array", "items": { "type": "string" } }
25
+ },
26
+ "additionalProperties": true
27
+ }
28
+ },
29
+ "blockers": { "type": "array", "items": { "type": "string" } }
30
+ },
31
+ "additionalProperties": true
32
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://sneakoscope.dev/schemas/image/image-artifact-path-contract.schema.json",
4
+ "title": "SKS Image Artifact Path Contract",
5
+ "type": "object",
6
+ "required": ["schema", "mission_id", "generated_at", "images", "blockers"],
7
+ "properties": {
8
+ "schema": { "const": "sks.image-artifact-path-contract.v1" },
9
+ "mission_id": { "type": "string", "minLength": 1 },
10
+ "generated_at": { "type": "string" },
11
+ "images": {
12
+ "type": "array",
13
+ "items": {
14
+ "type": "object",
15
+ "required": ["id", "kind", "file_path", "relative_path", "exists", "model_visible_path", "followup_edit_hint"],
16
+ "properties": {
17
+ "id": { "type": "string", "minLength": 1 },
18
+ "kind": { "enum": ["input_attachment", "generated_image", "edited_image", "visual_qa_snapshot"] },
19
+ "file_path": { "type": "string", "minLength": 1 },
20
+ "relative_path": { "type": "string" },
21
+ "exists": { "type": "boolean" },
22
+ "mime_type": { "type": ["string", "null"] },
23
+ "model_visible_path": { "type": "string", "minLength": 1 },
24
+ "followup_edit_hint": { "type": "string", "minLength": 1 }
25
+ },
26
+ "additionalProperties": true
27
+ }
28
+ },
29
+ "blockers": { "type": "array", "items": { "type": "string" } }
30
+ },
31
+ "additionalProperties": true
32
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://sneakoscope.dev/schemas/usage/codex-account-usage.schema.json",
4
+ "title": "SKS Codex Account Usage",
5
+ "type": "object",
6
+ "required": ["schema", "generated_at", "ok", "source", "blockers"],
7
+ "properties": {
8
+ "schema": { "const": "sks.codex-account-usage.v1" },
9
+ "generated_at": { "type": "string" },
10
+ "ok": { "type": "boolean" },
11
+ "source": { "enum": ["app-server", "unavailable", "fake"] },
12
+ "account_id": { "type": ["string", "null"] },
13
+ "token_usage": {
14
+ "type": ["object", "null"],
15
+ "properties": {
16
+ "input_tokens": { "type": "number" },
17
+ "output_tokens": { "type": "number" },
18
+ "total_tokens": { "type": "number" },
19
+ "reset_at": { "type": ["string", "null"] }
20
+ },
21
+ "additionalProperties": true
22
+ },
23
+ "usage_limit_tokens": { "type": ["number", "null"] },
24
+ "blockers": { "type": "array", "items": { "type": "string" } }
25
+ },
26
+ "additionalProperties": true
27
+ }