sneakoscope 2.0.18 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +125 -71
  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/mad-sks.js +2 -0
  8. package/dist/commands/zellij.js +58 -1
  9. package/dist/core/agents/agent-scheduler.js +32 -24
  10. package/dist/core/agents/native-cli-session-swarm.js +22 -2
  11. package/dist/core/codex-app/codex-app-handoff.js +30 -9
  12. package/dist/core/codex-app/codex-app-launcher.js +103 -0
  13. package/dist/core/codex-control/codex-0138-capability.js +42 -4
  14. package/dist/core/codex-control/codex-model-capabilities.js +25 -4
  15. package/dist/core/codex-control/codex-model-metadata.js +91 -0
  16. package/dist/core/codex-plugins/codex-plugin-cache.js +38 -0
  17. package/dist/core/codex-plugins/codex-plugin-diff.js +73 -0
  18. package/dist/core/codex-plugins/codex-plugin-json.js +35 -11
  19. package/dist/core/commands/mad-sks-command.js +4 -0
  20. package/dist/core/commands/naruto-command.js +27 -0
  21. package/dist/core/commands/qa-loop-command.js +41 -6
  22. package/dist/core/fsx.js +1 -1
  23. package/dist/core/image/image-artifact-path-contract.js +2 -0
  24. package/dist/core/image/image-artifact-registry.js +33 -0
  25. package/dist/core/image-ux-review/imagegen-adapter.js +27 -16
  26. package/dist/core/qa-loop/qa-loop-app-handoff-confirmation.js +51 -0
  27. package/dist/core/qa-loop/qa-loop-budget-policy.js +1 -1
  28. package/dist/core/qa-loop.js +44 -3
  29. package/dist/core/release/release-gate-cache-v2.js +47 -5
  30. package/dist/core/usage/codex-account-usage.js +77 -16
  31. package/dist/core/version.js +1 -1
  32. package/dist/core/zellij/zellij-slot-pane-renderer.js +5 -2
  33. package/dist/core/zellij/zellij-slot-telemetry.js +65 -12
  34. package/dist/core/zellij/zellij-ui-mode.js +8 -1
  35. package/dist/core/zellij/zellij-update.js +307 -0
  36. package/dist/core/zellij/zellij-worker-pane-manager.js +211 -145
  37. package/package.json +22 -2
  38. package/dist/core/naruto/naruto-work-stealing.js +0 -11
  39. package/dist/core/zellij/zellij-right-column-layout-proof.js +0 -42
@@ -17,11 +17,13 @@ import { runCodexAppHandoff, qaLoopShouldRequestAppHandoff } from '../codex-app/
17
17
  import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
18
18
  import { writeCodexAccountUsageArtifacts } from '../usage/codex-account-usage.js';
19
19
  import { buildQaLoopBudgetPolicy, selectQaLoopEscalatedEffort } from '../qa-loop/qa-loop-budget-policy.js';
20
+ import { writeCodexModelEffortCapabilityArtifact } from '../codex-control/codex-model-capabilities.js';
20
21
  import { discoverImageArtifactsInDir, writeImageArtifactPathContract } from '../image/image-artifact-path-contract.js';
21
22
  import { pluginAppTemplatePolicy } from '../codex-plugins/codex-plugin-json.js';
23
+ import { confirmQaLoopAppHandoff } from '../qa-loop/qa-loop-app-handoff-confirmation.js';
22
24
  import fsp from 'node:fs/promises';
23
25
  export async function qaLoopCommand(sub, args = []) {
24
- const known = new Set(['prepare', 'answer', 'run', 'status', 'help', '--help', '-h']);
26
+ const known = new Set(['prepare', 'answer', 'run', 'status', 'app-confirm', 'help', '--help', '-h']);
25
27
  const action = known.has(sub) ? sub : 'prepare';
26
28
  const actionArgs = action === 'prepare' && sub && !known.has(sub) ? [sub, ...args] : args;
27
29
  if (action === 'prepare')
@@ -32,12 +34,15 @@ export async function qaLoopCommand(sub, args = []) {
32
34
  return qaLoopRun(actionArgs);
33
35
  if (action === 'status')
34
36
  return qaLoopStatus(actionArgs);
37
+ if (action === 'app-confirm')
38
+ return qaLoopAppConfirm(actionArgs);
35
39
  console.log(`SKS QA-LOOP
36
40
 
37
41
  Usage:
38
42
  sks qa-loop prepare "target"
39
43
  sks qa-loop answer <mission-id|latest> <answers.json>
40
- sks qa-loop run <mission-id|latest> [--mock] [--max-cycles N] [--app-handoff] [--app-handoff-required]
44
+ sks qa-loop run <mission-id|latest> [--mock] [--max-cycles N] [--app-handoff] [--app-handoff-required] [--app-handoff-launch] [--app-handoff-artifact-only]
45
+ sks qa-loop app-confirm <mission-id|latest> --verdict pass|fail --notes "..."
41
46
  sks qa-loop status <mission-id|latest> [--desktop]
42
47
  `);
43
48
  }
@@ -146,9 +151,11 @@ async function qaLoopRun(args) {
146
151
  const usageArtifact = await writeCodexAccountUsageArtifacts(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), snapshot: null }));
147
152
  const budgetPolicy = buildQaLoopBudgetPolicy({ usage: usageArtifact?.snapshot || null, provider: 'codex-sdk' });
148
153
  await writeJsonAtomic(path.join(dir, 'qa-loop', 'qa-loop-budget-policy.json'), budgetPolicy);
154
+ const effortCapabilityArtifact = await writeCodexModelEffortCapabilityArtifact(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), capability: null }));
149
155
  const effortEscalation = selectQaLoopEscalatedEffort({
150
156
  failureCount: Number(qaGate.safe_fix_attempts || qaGate.failure_count || 0),
151
- currentEffort: String(profile || 'high').replace(/^sks-(?:logic|agent)-/, '').replace(/-fast$/, '') || 'high'
157
+ currentEffort: String(profile || 'high').replace(/^sks-(?:logic|agent)-/, '').replace(/-fast$/, '') || 'high',
158
+ capability: effortCapabilityArtifact?.capability || undefined
152
159
  });
153
160
  await writeJsonAtomic(path.join(dir, 'qa-loop', 'qa-loop-effort-escalation.json'), effortEscalation);
154
161
  const discoveredImages = await discoverImageArtifactsInDir(dir).catch(() => []);
@@ -158,6 +165,9 @@ async function qaLoopRun(args) {
158
165
  const pluginInventory = await readJson(path.join(root, '.sneakoscope', 'codex-plugin-inventory.json'), null);
159
166
  const pluginPolicy = pluginInventory?.schema === 'sks.codex-plugin-inventory.v1' ? pluginAppTemplatePolicy(pluginInventory) : null;
160
167
  const appHandoffRequired = flag(args, '--app-handoff-required') || process.env.SKS_QA_LOOP_APP_HANDOFF_REQUIRED === '1';
168
+ const launchMode = flag(args, '--app-handoff-launch') || process.env.SKS_QA_LOOP_APP_HANDOFF_LAUNCH === '1'
169
+ ? 'attempt-launch'
170
+ : 'artifact-only';
161
171
  const appHandoffRequested = qaLoopShouldRequestAppHandoff({
162
172
  args,
163
173
  uiRequired,
@@ -183,13 +193,15 @@ async function qaLoopRun(args) {
183
193
  ].filter(Boolean),
184
194
  prompt: mission.prompt || 'QA-LOOP desktop handoff',
185
195
  require_desktop: appHandoffRequired,
186
- capability_required: 'codex-0.138'
196
+ capability_required: 'codex-0.138',
197
+ launch_mode: flag(args, '--app-handoff-artifact-only') ? 'artifact-only' : launchMode
187
198
  }).catch((err) => ({
188
199
  ok: false,
189
200
  status: 'blocked_for_desktop_review',
190
201
  artifact_path: path.join(dir, 'qa-loop', 'app-handoff.json'),
191
202
  blockers: [`codex_app_handoff_failed:${err?.message || String(err)}`],
192
- desktop_handoff_supported: false
203
+ desktop_handoff_supported: false,
204
+ launch_attempt: null
193
205
  }))
194
206
  : null;
195
207
  if (appHandoff || imagePathContract) {
@@ -200,6 +212,9 @@ async function qaLoopRun(args) {
200
212
  desktop_app_handoff_status: appHandoff ? appHandoff.status : 'not_requested',
201
213
  desktop_app_handoff_artifact: appHandoff ? path.relative(dir, appHandoff.artifact_path) : null,
202
214
  desktop_app_handoff_supported: appHandoff ? appHandoff.desktop_handoff_supported === true : false,
215
+ desktop_app_handoff_confirmed: latestGate.desktop_app_handoff_confirmed === true,
216
+ desktop_app_handoff_verdict: latestGate.desktop_app_handoff_verdict || null,
217
+ desktop_app_handoff_launch_attempt: appHandoff ? appHandoff.launch_attempt || null : null,
203
218
  desktop_app_handoff_is_web_ui_evidence: false,
204
219
  image_artifact_path_contract_present: Boolean(imagePathContract),
205
220
  image_artifact_path_contract_artifact: imagePathContract ? 'qa-loop/image-artifact-path-contract.json' : null,
@@ -207,6 +222,7 @@ async function qaLoopRun(args) {
207
222
  blockers: Array.from(new Set([
208
223
  ...(latestGate.blockers || []),
209
224
  ...(appHandoffRequired && appHandoff && appHandoff.ok !== true ? ['blocked_for_desktop_review'] : []),
225
+ ...(appHandoffRequired && latestGate.desktop_app_handoff_confirmed !== true ? ['desktop_app_handoff_confirmation_missing'] : []),
210
226
  ...(imagePathContract?.contract?.blockers || [])
211
227
  ])),
212
228
  notes: [
@@ -367,8 +383,10 @@ async function qaLoopStatus(args) {
367
383
  const nativeAgentPlan = await readJson(path.join(dir, 'qa-agent-plan.json'), null);
368
384
  const agentSessions = await readJson(path.join(dir, 'agents', 'agent-sessions.json'), null);
369
385
  const desktop = await readJson(path.join(dir, 'qa-loop', 'app-handoff.json'), null);
386
+ const desktopConfirmation = await readJson(path.join(dir, 'qa-loop', 'app-handoff-confirmation.json'), null);
387
+ const desktopReviewComplete = desktopConfirmation?.verdict === 'pass';
370
388
  if (flag(args, '--json'))
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));
389
+ return console.log(JSON.stringify({ mission, state, qa: status, desktop_app_handoff: desktop, desktop_app_confirmation: desktopConfirmation, desktop_review_complete: desktopReviewComplete, native_agent_plan: nativeAgentPlan, agent_sessions: agentSessions?.sessions || null }, null, 2));
372
390
  console.log('SKS QA-LOOP Status\n');
373
391
  console.log(`Mission: ${id}`);
374
392
  console.log(`Phase: ${state.phase || mission.phase}`);
@@ -380,11 +398,28 @@ async function qaLoopStatus(args) {
380
398
  if (flag(args, '--desktop')) {
381
399
  console.log('Desktop:');
382
400
  console.log(` /app handoff: ${desktop?.status || 'not_requested'}`);
401
+ console.log(` launch: ${desktop?.launch_attempt?.attempted ? desktop?.launch_attempt?.launched ? 'launched' : 'attempted_fallback' : 'not_attempted'}`);
402
+ console.log(` confirmation: ${desktopConfirmation?.verdict || 'missing'}`);
403
+ console.log(` complete: ${desktopReviewComplete ? 'yes' : 'no'}`);
383
404
  if (desktop?.operator_instruction?.prompt_artifact)
384
405
  console.log(` prompt: ${desktop.operator_instruction.prompt_artifact}`);
385
406
  console.log(' web evidence: not a substitute for Codex Chrome Extension web UI verification');
386
407
  }
387
408
  }
409
+ async function qaLoopAppConfirm(args) {
410
+ const root = await sksRoot();
411
+ const id = await resolveMissionId(root, args[0]);
412
+ const verdict = String(readFlagValue(args, '--verdict', '') || '').trim();
413
+ const notes = String(readFlagValue(args, '--notes', '') || '');
414
+ if (!id || !['pass', 'fail'].includes(verdict))
415
+ throw new Error('Usage: sks qa-loop app-confirm <mission-id|latest> --verdict pass|fail --notes "..."');
416
+ const result = await confirmQaLoopAppHandoff(root, { missionId: id, verdict: verdict, notes });
417
+ const evaluated = await evaluateQaGate(path.join(root, '.sneakoscope', 'missions', id));
418
+ if (flag(args, '--json'))
419
+ return console.log(JSON.stringify({ schema: 'sks.qa-loop-app-confirm.v1', ok: verdict === 'pass', mission_id: id, confirmation: result.confirmation, artifact_path: result.artifact_path, gate: result.gate, evaluated }, null, 2));
420
+ console.log(`QA-LOOP Desktop app handoff confirmation recorded: ${id} (${verdict})`);
421
+ console.log(path.relative(root, result.artifact_path));
422
+ }
388
423
  async function writeQaLoopImagePathContract(root, dir, missionId, images) {
389
424
  const primary = await writeImageArtifactPathContract(root, {
390
425
  missionId,
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.18';
8
+ export const PACKAGE_VERSION = '3.0.0';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
@@ -16,6 +16,8 @@ export async function buildImageArtifactPathContract(root, input) {
16
16
  kind: image.kind,
17
17
  file_path: filePath,
18
18
  relative_path: path.relative(root, filePath),
19
+ route: image.route || null,
20
+ stage: image.stage || null,
19
21
  exists,
20
22
  mime_type: mimeForPath(filePath),
21
23
  width: dims?.width ?? null,
@@ -0,0 +1,33 @@
1
+ import path from 'node:path';
2
+ import { readJson, writeJsonAtomic } from '../fsx.js';
3
+ import { buildImageArtifactPathContract } from './image-artifact-path-contract.js';
4
+ export async function registerImageArtifact(root, input) {
5
+ const artifactPath = imageArtifactRegistryPath(root, input.missionId);
6
+ const existing = await readJson(artifactPath, null);
7
+ const id = input.id || path.basename(input.filePath).replace(/[^0-9A-Za-z._-]/g, '_');
8
+ const rows = [
9
+ ...(existing?.images || [])
10
+ .filter((image) => image.id !== id)
11
+ .map((image) => ({
12
+ id: image.id,
13
+ kind: image.kind,
14
+ filePath: image.file_path,
15
+ route: image.route || null,
16
+ stage: image.stage || null
17
+ })),
18
+ {
19
+ id,
20
+ kind: input.kind,
21
+ filePath: input.filePath,
22
+ route: input.route,
23
+ stage: input.stage
24
+ }
25
+ ];
26
+ const contract = await buildImageArtifactPathContract(root, { missionId: input.missionId, images: rows });
27
+ await writeJsonAtomic(artifactPath, contract);
28
+ return contract;
29
+ }
30
+ export function imageArtifactRegistryPath(root, missionId) {
31
+ return path.join(root, '.sneakoscope', 'missions', missionId, 'image-artifact-path-contract.json');
32
+ }
33
+ //# sourceMappingURL=image-artifact-registry.js.map
@@ -1,13 +1,14 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
3
  import { parseShellEnvValue } from '../codex-lb/codex-lb-env.js';
4
- import { ensureDir, exists, nowIso, readJson, readText, writeJsonAtomic } from '../fsx.js';
4
+ import { ensureDir, exists, nowIso, projectRoot, readJson, readText, writeJsonAtomic } from '../fsx.js';
5
5
  import { sha256File, imageDimensions } from '../wiki-image/image-hash.js';
6
6
  import { detectImagegenCapability } from '../imagegen/imagegen-capability.js';
7
7
  import { validateGptImage2Request } from '../imagegen/gpt-image-2-request-validator.js';
8
8
  import { withResponsesRetry } from '../responses-retry-policy.js';
9
9
  import { discoverCodexAppGeneratedImage } from './codex-app-generated-image-discovery.js';
10
10
  import { writeImageArtifactPathContract } from '../image/image-artifact-path-contract.js';
11
+ import { registerImageArtifact } from '../image/image-artifact-registry.js';
11
12
  const DEFAULT_OPENAI_IMAGE_EDITS_ENDPOINT = 'https://api.openai.com/v1/images/edits';
12
13
  export function buildCalloutPrompt(sourceScreenId, context = {}) {
13
14
  return [
@@ -479,16 +480,36 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
479
480
  };
480
481
  }
481
482
  async function writeGeneratedImagePathContract(input, outputPath, provider) {
482
- return writeImageArtifactPathContract(process.cwd(), {
483
+ const root = await resolveImageArtifactRoot(input);
484
+ if (input.mission_id) {
485
+ await registerImageArtifact(root, {
486
+ missionId: input.mission_id,
487
+ id: `${provider}-${input.source_screen_id || 'screen'}`,
488
+ kind: 'generated_image',
489
+ filePath: outputPath,
490
+ route: '$Image-UX-Review',
491
+ stage: provider
492
+ });
493
+ }
494
+ return writeImageArtifactPathContract(root, {
483
495
  missionId: input.mission_id || 'unassigned',
484
496
  images: [{
485
497
  id: `${provider}-${input.source_screen_id || 'screen'}`,
486
498
  kind: 'generated_image',
487
- filePath: outputPath
499
+ filePath: outputPath,
500
+ route: '$Image-UX-Review',
501
+ stage: provider
488
502
  }],
489
503
  artifactPath: path.join(input.output_dir, 'image-artifact-path-contract.json')
490
504
  });
491
505
  }
506
+ async function resolveImageArtifactRoot(input) {
507
+ const cwdRoot = await projectRoot(process.cwd()).catch(() => process.cwd());
508
+ const resolvedCwd = path.resolve(process.cwd());
509
+ if (path.resolve(cwdRoot) !== resolvedCwd)
510
+ return cwdRoot;
511
+ return projectRoot(input.output_dir || process.cwd()).catch(() => cwdRoot);
512
+ }
492
513
  export async function generateGptImage2CalloutReview(input, opts = {}) {
493
514
  if (opts.fake === true || process.env.SKS_TEST_FAKE_IMAGEGEN === '1') {
494
515
  return createFakeImagegenAdapter(opts.fakeAdapter || {}).generateCalloutReview(input);
@@ -500,21 +521,11 @@ export async function generateGptImage2CalloutReview(input, opts = {}) {
500
521
  // allowApiFallback:false or SKS_IMAGEGEN_ALLOW_API_FALLBACK=0.
501
522
  const openAiKeyPresent = Boolean(opts.openai?.apiKey || process.env.OPENAI_API_KEY);
502
523
  const explicitDisableApiFallback = opts.allowApiFallback === false || process.env.SKS_IMAGEGEN_ALLOW_API_FALLBACK === '0';
503
- // codex-lb imagegen routes gpt-image-2 through the same Codex /responses
504
- // backend the LB already proxies (base_url ends in /backend-api/codex, so the
505
- // image_generation tool call is just another Responses request). When codex-lb
506
- // is the active, fully-configured auth (selected provider + key + base_url) and
507
- // there is no direct OPENAI_API_KEY, enable it BY DEFAULT so image generation
508
- // works for users authenticated only through codex-lb — that is the common case
509
- // and a hard block here is the bug the user hit. It still never overrides a real
510
- // OpenAI key, and SKS_IMAGEGEN_ALLOW_CODEX_LB_API_FALLBACK=0 (or
511
- // allowCodexLbApiFallback:false) opts out for callers that require Codex App
512
- // built-in evidence only.
513
- const codexLbAuthActive = capability?.codex_lb?.available === true;
524
+ // codex-lb imagegen is a direct API fallback, not Codex App imagegen evidence.
525
+ // It must be explicitly enabled by the caller or environment.
514
526
  const explicitDisableCodexLbFallback = opts.allowCodexLbApiFallback === false || process.env.SKS_IMAGEGEN_ALLOW_CODEX_LB_API_FALLBACK === '0';
515
527
  const allowCodexLbApiFallback = !explicitDisableCodexLbFallback && (opts.allowCodexLbApiFallback === true
516
- || process.env.SKS_IMAGEGEN_ALLOW_CODEX_LB_API_FALLBACK === '1'
517
- || (codexLbAuthActive && !openAiKeyPresent));
528
+ || process.env.SKS_IMAGEGEN_ALLOW_CODEX_LB_API_FALLBACK === '1');
518
529
  const allowApiFallback = !explicitDisableApiFallback && (opts.allowApiFallback === true
519
530
  || process.env.SKS_IMAGEGEN_ALLOW_API_FALLBACK === '1'
520
531
  || openAiKeyPresent
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
3
+ export async function confirmQaLoopAppHandoff(root, input) {
4
+ const missionDir = path.join(root, '.sneakoscope', 'missions', input.missionId);
5
+ const qaLoopDir = path.join(missionDir, 'qa-loop');
6
+ const handoffArtifact = path.join(qaLoopDir, 'app-handoff.json');
7
+ const confirmationArtifact = path.join(qaLoopDir, 'app-handoff-confirmation.json');
8
+ const handoff = await readJson(handoffArtifact, null);
9
+ if (handoff?.schema !== 'sks.codex-app-handoff-result.v1') {
10
+ throw new Error(`Cannot confirm Desktop app handoff before app-handoff.json exists for mission ${input.missionId}`);
11
+ }
12
+ if (input.verdict === 'pass' && handoff.status === 'blocked_for_desktop_review') {
13
+ throw new Error(`Cannot pass-confirm blocked Desktop app handoff for mission ${input.missionId}`);
14
+ }
15
+ const confirmation = {
16
+ schema: 'sks.qa-loop-app-handoff-confirmation.v1',
17
+ mission_id: input.missionId,
18
+ confirmed_at: nowIso(),
19
+ verdict: input.verdict,
20
+ notes: String(input.notes || ''),
21
+ operator: input.operator || process.env.USER || null,
22
+ related_handoff_artifact: path.relative(missionDir, handoffArtifact).split(path.sep).join('/')
23
+ };
24
+ await writeJsonAtomic(confirmationArtifact, confirmation);
25
+ const gatePath = path.join(missionDir, 'qa-gate.json');
26
+ const previousGate = await readJson(gatePath, {});
27
+ const previousBlockers = Array.isArray(previousGate.blockers) ? previousGate.blockers : [];
28
+ const failedBlocker = 'desktop_app_handoff_failed';
29
+ const blockers = input.verdict === 'pass'
30
+ ? previousBlockers.filter((blocker) => blocker !== failedBlocker && blocker !== 'desktop_app_handoff_confirmation_missing')
31
+ : Array.from(new Set([...previousBlockers, failedBlocker]));
32
+ const gate = {
33
+ ...previousGate,
34
+ desktop_app_handoff_required: previousGate.desktop_app_handoff_required === true,
35
+ desktop_app_handoff_status: input.verdict === 'pass' ? 'completed' : previousGate.desktop_app_handoff_status || 'pending',
36
+ desktop_app_handoff_confirmed: input.verdict === 'pass',
37
+ desktop_app_handoff_verdict: input.verdict,
38
+ desktop_app_handoff_confirmation_artifact: path.relative(missionDir, confirmationArtifact).split(path.sep).join('/'),
39
+ desktop_app_handoff_confirmation_notes: confirmation.notes,
40
+ blockers,
41
+ notes: Array.from(new Set([
42
+ ...(Array.isArray(previousGate.notes) ? previousGate.notes : []),
43
+ input.verdict === 'pass'
44
+ ? 'Codex Desktop /app review was explicitly confirmed by operator artifact.'
45
+ : 'Codex Desktop /app review failed and remains a QA blocker.'
46
+ ]))
47
+ };
48
+ await writeJsonAtomic(gatePath, gate);
49
+ return { confirmation, artifact_path: confirmationArtifact, gate };
50
+ }
51
+ //# sourceMappingURL=qa-loop-app-handoff-confirmation.js.map
@@ -9,7 +9,7 @@ export function buildQaLoopBudgetPolicy(input = {}) {
9
9
  const baseBudget = defaultModelCallBudget(String(input.provider || 'codex-sdk'));
10
10
  return {
11
11
  schema: 'sks.qa-loop-budget-policy.v1',
12
- ok: true,
12
+ ok: available,
13
13
  account_usage_source: usage?.source || 'unavailable',
14
14
  token_usage_available: available,
15
15
  near_limit: nearLimit,
@@ -313,6 +313,9 @@ export function defaultQaGate(contract = {}, opts = {}) {
313
313
  desktop_app_handoff_status: 'not_requested',
314
314
  desktop_app_handoff_artifact: null,
315
315
  desktop_app_handoff_supported: false,
316
+ desktop_app_handoff_confirmed: false,
317
+ desktop_app_handoff_verdict: null,
318
+ desktop_app_handoff_confirmation_artifact: null,
316
319
  desktop_app_handoff_is_web_ui_evidence: false,
317
320
  image_artifact_path_contract_present: false,
318
321
  image_artifact_path_contract_artifact: null,
@@ -383,8 +386,14 @@ export async function evaluateQaGate(dir) {
383
386
  reasons.push('computer_use_web_evidence_forbidden');
384
387
  }
385
388
  if (gate.desktop_app_handoff_required === true) {
386
- if (gate.desktop_app_handoff_status !== 'pending' && gate.desktop_app_handoff_status !== 'completed')
389
+ if (!['pending', 'launched_pending_confirmation', 'completed'].includes(String(gate.desktop_app_handoff_status || '')))
387
390
  reasons.push('desktop_app_handoff_missing');
391
+ if (gate.desktop_app_handoff_confirmed !== true)
392
+ reasons.push('desktop_app_handoff_confirmation_missing');
393
+ if (gate.desktop_app_handoff_verdict !== 'pass')
394
+ reasons.push('desktop_app_handoff_verdict_not_pass');
395
+ if (gate.desktop_app_handoff_status !== 'completed')
396
+ reasons.push('desktop_app_handoff_not_completed');
388
397
  if (gate.desktop_app_handoff_is_web_ui_evidence === true)
389
398
  reasons.push('desktop_app_handoff_misused_as_web_evidence');
390
399
  }
@@ -410,7 +419,38 @@ export async function writeMockQaResult(dir, mission, contract) {
410
419
  const reportFile = isQaReportFilename(previousReportFile) ? previousReportFile : qaReportFilename();
411
420
  const uiRequired = qaUiRequired(contract.answers || {});
412
421
  await writeTextAtomic(path.join(dir, reportFile), `# QA-LOOP Report\n\nMission: ${mission.id}\nMode: mock verification\n\nMock QA-LOOP completed. No live UI/API actions were executed.\n\n## Honest Mode\n\nThis is a mock smoke run for command verification, not production QA evidence.\n`);
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.'] });
422
+ await writeJsonAtomic(path.join(dir, 'qa-gate.json'), {
423
+ ...defaultQaGate(contract, { reportFile }),
424
+ desktop_app_handoff_required: previousGate.desktop_app_handoff_required === true,
425
+ desktop_app_handoff_status: previousGate.desktop_app_handoff_status || 'not_requested',
426
+ desktop_app_handoff_artifact: previousGate.desktop_app_handoff_artifact || null,
427
+ desktop_app_handoff_supported: previousGate.desktop_app_handoff_supported === true,
428
+ desktop_app_handoff_confirmed: previousGate.desktop_app_handoff_confirmed === true,
429
+ desktop_app_handoff_verdict: previousGate.desktop_app_handoff_verdict || null,
430
+ desktop_app_handoff_confirmation_artifact: previousGate.desktop_app_handoff_confirmation_artifact || null,
431
+ desktop_app_handoff_is_web_ui_evidence: false,
432
+ image_artifact_path_contract_present: previousGate.image_artifact_path_contract_present === true,
433
+ image_artifact_path_contract_artifact: previousGate.image_artifact_path_contract_artifact || null,
434
+ image_artifact_path_contract_blockers: previousGate.image_artifact_path_contract_blockers || [],
435
+ blockers: previousGate.blockers || [],
436
+ passed: !uiRequired,
437
+ qa_report_written: true,
438
+ qa_ledger_complete: true,
439
+ checklist_completed: true,
440
+ safety_reviewed: true,
441
+ credentials_not_persisted: true,
442
+ chrome_extension_preflight_passed: !uiRequired,
443
+ ui_chrome_extension_evidence: !uiRequired,
444
+ ui_computer_use_evidence: false,
445
+ ui_evidence_source: uiRequired ? null : 'not_required',
446
+ unresolved_findings: 0,
447
+ unresolved_fixable_findings: 0,
448
+ unsafe_or_deferred_findings: 0,
449
+ post_fix_verification_complete: true,
450
+ honest_mode_complete: true,
451
+ evidence: ['mock QA-LOOP smoke completed'],
452
+ notes: ['No live UI/API verification was claimed.']
453
+ });
414
454
  return evaluateQaGate(dir);
415
455
  }
416
456
  export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile, imagePathContract, appHandoff }) {
@@ -442,10 +482,11 @@ export async function qaStatus(dir) {
442
482
  const gate = await readJson(path.join(dir, 'qa-gate.evaluated.json'), await readJson(path.join(dir, 'qa-gate.json'), null));
443
483
  const ledger = await readJson(path.join(dir, 'qa-ledger.json'), null);
444
484
  const appHandoff = await readJson(path.join(dir, 'qa-loop', 'app-handoff.json'), null);
485
+ const appConfirmation = await readJson(path.join(dir, 'qa-loop', 'app-handoff-confirmation.json'), null);
445
486
  const imagePathContract = await readJson(path.join(dir, 'qa-loop', 'image-artifact-path-contract.json'), null);
446
487
  const reportFile = qaReportFileFromGate(gate?.gate || gate || {}) || ledger?.qa_report_file || null;
447
488
  const report = reportFile && isQaReportFilename(reportFile) ? await readText(path.join(dir, reportFile), '') : '';
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 };
489
+ return { gate, checklist_count: ledger?.checklist?.length ?? null, report_file: reportFile, report_written: Boolean(report.trim()), desktop_app_handoff: appHandoff, desktop_app_confirmation: appConfirmation, desktop_review_complete: appConfirmation?.verdict === 'pass', image_path_contract: imagePathContract };
449
490
  }
450
491
  function qaChecklist(a) {
451
492
  const cases = [
@@ -5,19 +5,44 @@ export const RELEASE_GATE_CACHE_V2_SCHEMA = 'sks.release-gate-cache.v2';
5
5
  export function releaseGateCacheFile(root) {
6
6
  return path.join(root, '.sneakoscope', 'reports', 'release-gates', 'cache-v2.json');
7
7
  }
8
+ // Files whose only release-to-release difference is the version literal.
9
+ // Hashing them version-neutrally keeps a pure `sks versioning bump` from
10
+ // invalidating every behavior gate: bumping the version rewrites
11
+ // package.json, package-lock.json, and the three PACKAGE_VERSION constant
12
+ // sources, which are inputs of ~280 gates (via `package.json` and `src/**`).
13
+ // Before this normalization every publish re-ran the entire DAG from zero
14
+ // (test:blackbox alone is ~11 minutes) even when no behavior changed.
15
+ // Version-CORRECTNESS gates (release:version-truth, release:metadata, ...)
16
+ // are declared with `cache.enabled: false`, so they always re-run and still
17
+ // catch version drift. Set SKS_RELEASE_CACHE_VERSION_SENSITIVE=1 to restore
18
+ // the old fully version-sensitive hashing.
19
+ const VERSION_NEUTRAL_CACHE_FILES = new Set([
20
+ 'package.json',
21
+ 'package-lock.json',
22
+ 'src/core/version.ts',
23
+ 'src/core/fsx.ts',
24
+ 'src/bin/sks.ts'
25
+ ]);
8
26
  export function releaseGateCacheKey(root, gate) {
9
27
  const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
28
+ const releaseVersion = String(pkg.version || '');
29
+ const versionSensitive = process.env.SKS_RELEASE_CACHE_VERSION_SENSITIVE === '1';
10
30
  const hash = crypto.createHash('sha256');
11
31
  hash.update(gate.id);
12
32
  hash.update(gate.command);
13
- hash.update(String(pkg.version || ''));
33
+ if (versionSensitive)
34
+ hash.update(releaseVersion);
14
35
  hash.update(process.version);
15
36
  hash.update(String(process.env.npm_config_user_agent || ''));
16
37
  hash.update(JSON.stringify(gate.resource || []));
17
38
  hash.update(JSON.stringify(gate.preset || []));
18
39
  hashFileIfPresent(hash, path.join(root, 'release-gates.v2.json'));
19
- hashFileIfPresent(hash, path.join(root, 'package.json'));
20
- hashFileIfPresent(hash, path.join(root, 'dist', 'build-manifest.json'));
40
+ if (versionSensitive || !gate.cache.inputs.length) {
41
+ // No declared inputs (or explicitly version-sensitive mode): fall back to
42
+ // the conservative global digests so such a gate cannot cache-hit forever.
43
+ hashFileIfPresent(hash, path.join(root, 'package.json'));
44
+ hashFileIfPresent(hash, path.join(root, 'dist', 'build-manifest.json'));
45
+ }
21
46
  for (const input of gate.cache.inputs) {
22
47
  const expanded = expandGlob(root, input);
23
48
  hash.update(`input:${input}`);
@@ -26,12 +51,29 @@ export function releaseGateCacheKey(root, gate) {
26
51
  continue;
27
52
  }
28
53
  for (const file of expanded) {
29
- hash.update(path.relative(root, file));
30
- hashFileIfPresent(hash, file);
54
+ const rel = path.relative(root, file);
55
+ hash.update(rel);
56
+ if (!versionSensitive && VERSION_NEUTRAL_CACHE_FILES.has(rel))
57
+ hashVersionNeutralFile(hash, file, releaseVersion);
58
+ else
59
+ hashFileIfPresent(hash, file);
31
60
  }
32
61
  }
33
62
  return hash.digest('hex');
34
63
  }
64
+ function hashVersionNeutralFile(hash, file, releaseVersion) {
65
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile())
66
+ return;
67
+ const text = fs.readFileSync(file, 'utf8');
68
+ if (!releaseVersion) {
69
+ hash.update(text);
70
+ return;
71
+ }
72
+ // Replace exact occurrences of the current release version literal so a
73
+ // version-only bump hashes identically. Any other content change in these
74
+ // files still alters the key.
75
+ hash.update(text.split(releaseVersion).join('__SKS_RELEASE_VERSION__'));
76
+ }
35
77
  export function expandGlob(root, input) {
36
78
  const absolute = path.join(root, input);
37
79
  if (!/[*!?[\]{}]/.test(input)) {
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
- import { nowIso, writeJsonAtomic } from '../fsx.js';
2
+ import { findCodexBinary } from '../codex-adapter.js';
3
+ import { nowIso, runProcess, writeJsonAtomic } from '../fsx.js';
3
4
  export async function collectCodexAccountUsage() {
4
5
  if (process.env.SKS_CODEX_ACCOUNT_USAGE_FAKE === '1') {
5
6
  return {
@@ -15,22 +16,42 @@ export async function collectCodexAccountUsage() {
15
16
  reset_at: null
16
17
  },
17
18
  usage_limit_tokens: 100000,
19
+ attempted_sources: ['fake'],
18
20
  blockers: []
19
21
  };
20
22
  }
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)}`]);
23
+ const attemptedSources = [];
24
+ const urls = [
25
+ ['CODEX_APP_SERVER_USAGE_URL', process.env.CODEX_APP_SERVER_USAGE_URL],
26
+ ['SKS_CODEX_APP_SERVER_USAGE_URL', process.env.SKS_CODEX_APP_SERVER_USAGE_URL],
27
+ ...localWellKnownUsageUrls().map((url) => [`local:${url}`, url])
28
+ ];
29
+ const blockers = [];
30
+ for (const [label, rawUrl] of urls) {
31
+ const url = String(rawUrl || '').trim();
32
+ if (!url)
33
+ continue;
34
+ attemptedSources.push(label);
35
+ try {
36
+ const response = await fetch(url, { signal: AbortSignal.timeout(label.startsWith('local:') ? 800 : 5000) });
37
+ if (!response.ok) {
38
+ blockers.push(`codex_app_server_usage_http_${response.status}:${label}`);
39
+ continue;
40
+ }
41
+ const payload = await response.json();
42
+ return normalizeUsagePayload(payload, 'app-server', attemptedSources);
43
+ }
44
+ catch (err) {
45
+ blockers.push(`codex_app_server_usage_fetch_failed:${label}:${err?.message || String(err)}`);
46
+ }
33
47
  }
48
+ const cli = await collectUsageFromCodexCli(attemptedSources).catch((err) => {
49
+ blockers.push(`codex_cli_usage_probe_failed:${err?.message || String(err)}`);
50
+ return null;
51
+ });
52
+ if (cli)
53
+ return cli;
54
+ return unavailable(attemptedSources.length ? blockers : ['codex_app_server_usage_endpoint_unavailable'], attemptedSources);
34
55
  }
35
56
  export async function writeCodexAccountUsageArtifacts(root, input = {}) {
36
57
  const snapshot = await collectCodexAccountUsage();
@@ -43,7 +64,7 @@ export async function writeCodexAccountUsageArtifacts(root, input = {}) {
43
64
  }
44
65
  return { snapshot, root_artifact: rootArtifact, mission_artifact: missionArtifact };
45
66
  }
46
- function normalizeUsagePayload(payload, source) {
67
+ function normalizeUsagePayload(payload, source, attemptedSources) {
47
68
  const usage = payload?.token_usage || payload?.usage || payload;
48
69
  const input = Number(usage?.input_tokens || usage?.inputTokens || 0);
49
70
  const output = Number(usage?.output_tokens || usage?.outputTokens || 0);
@@ -61,18 +82,58 @@ function normalizeUsagePayload(payload, source) {
61
82
  reset_at: usage?.reset_at || usage?.resetAt || null
62
83
  },
63
84
  usage_limit_tokens: Number.isFinite(Number(payload?.usage_limit_tokens || payload?.usageLimitTokens)) ? Number(payload?.usage_limit_tokens || payload?.usageLimitTokens) : null,
85
+ attempted_sources: attemptedSources,
64
86
  blockers: []
65
87
  };
66
88
  }
67
- function unavailable(blockers) {
89
+ function unavailable(blockers, attemptedSources = []) {
68
90
  return {
69
91
  schema: 'sks.codex-account-usage.v1',
70
92
  generated_at: nowIso(),
71
- ok: true,
93
+ ok: false,
72
94
  source: 'unavailable',
73
95
  token_usage: null,
74
96
  usage_limit_tokens: null,
97
+ attempted_sources: attemptedSources,
75
98
  blockers
76
99
  };
77
100
  }
101
+ function localWellKnownUsageUrls() {
102
+ const ports = [
103
+ process.env.CODEX_APP_SERVER_PORT,
104
+ process.env.SKS_CODEX_APP_SERVER_PORT,
105
+ 1455,
106
+ 1456,
107
+ 3000
108
+ ].map((value) => Number(value)).filter((value, index, rows) => Number.isFinite(value) && value > 0 && rows.indexOf(value) === index);
109
+ return ports.flatMap((port) => [
110
+ `http://127.0.0.1:${port}/usage`,
111
+ `http://127.0.0.1:${port}/api/usage`,
112
+ `http://127.0.0.1:${port}/.well-known/codex/usage`
113
+ ]);
114
+ }
115
+ async function collectUsageFromCodexCli(attemptedSources) {
116
+ const bin = await findCodexBinary();
117
+ if (!bin)
118
+ return null;
119
+ const commands = [
120
+ ['account', 'usage', '--json'],
121
+ ['usage', '--json'],
122
+ ['app-server', 'status', '--json']
123
+ ];
124
+ for (const args of commands) {
125
+ const label = `codex-cli:${args.join(' ')}`;
126
+ attemptedSources.push(label);
127
+ const result = await runProcess(bin, args, { timeoutMs: 3000, maxOutputBytes: 64 * 1024 }).catch(() => null);
128
+ if (!result || result.code !== 0)
129
+ continue;
130
+ try {
131
+ const payload = JSON.parse(`${result.stdout || ''}${result.stderr || ''}`.trim() || '{}');
132
+ const normalized = normalizeUsagePayload(payload, 'app-server', attemptedSources);
133
+ return { ...normalized, source: 'app-server' };
134
+ }
135
+ catch { }
136
+ }
137
+ return null;
138
+ }
78
139
  //# sourceMappingURL=codex-account-usage.js.map
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '2.0.18';
1
+ export const PACKAGE_VERSION = '3.0.0';
2
2
  //# sourceMappingURL=version.js.map