sneakoscope 2.0.18 → 3.0.1

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 (41) hide show
  1. package/README.md +127 -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-0139-capability.js +102 -0
  15. package/dist/core/codex-control/codex-model-capabilities.js +25 -4
  16. package/dist/core/codex-control/codex-model-metadata.js +91 -0
  17. package/dist/core/codex-plugins/codex-plugin-cache.js +38 -0
  18. package/dist/core/codex-plugins/codex-plugin-diff.js +73 -0
  19. package/dist/core/codex-plugins/codex-plugin-json.js +35 -11
  20. package/dist/core/commands/mad-sks-command.js +8 -0
  21. package/dist/core/commands/naruto-command.js +29 -0
  22. package/dist/core/commands/qa-loop-command.js +41 -6
  23. package/dist/core/fsx.js +1 -1
  24. package/dist/core/image/image-artifact-path-contract.js +2 -0
  25. package/dist/core/image/image-artifact-registry.js +33 -0
  26. package/dist/core/image-ux-review/imagegen-adapter.js +27 -16
  27. package/dist/core/pipeline-internals/runtime-core.js +4 -2
  28. package/dist/core/qa-loop/qa-loop-app-handoff-confirmation.js +51 -0
  29. package/dist/core/qa-loop/qa-loop-budget-policy.js +1 -1
  30. package/dist/core/qa-loop.js +44 -3
  31. package/dist/core/release/release-gate-cache-v2.js +47 -5
  32. package/dist/core/usage/codex-account-usage.js +77 -16
  33. package/dist/core/version.js +1 -1
  34. package/dist/core/zellij/zellij-slot-pane-renderer.js +5 -2
  35. package/dist/core/zellij/zellij-slot-telemetry.js +65 -12
  36. package/dist/core/zellij/zellij-ui-mode.js +8 -1
  37. package/dist/core/zellij/zellij-update.js +307 -0
  38. package/dist/core/zellij/zellij-worker-pane-manager.js +211 -145
  39. package/package.json +23 -2
  40. package/dist/core/naruto/naruto-work-stealing.js +0 -11
  41. package/dist/core/zellij/zellij-right-column-layout-proof.js +0 -42
@@ -19,14 +19,18 @@ export async function runCodexPluginDetailJson(pluginId) {
19
19
  return runCodexJson(bin, ['plugin', 'detail', pluginId, '--json']);
20
20
  }
21
21
  export async function buildCodexPluginInventory() {
22
+ const started = Date.now();
22
23
  const capability = await detectCodex0138Capability();
23
24
  const listJson = await runCodexPluginListJson();
24
25
  const summaries = normalizePluginList(listJson);
25
- const plugins = [];
26
- for (const summary of summaries) {
26
+ const concurrency = Math.max(1, Number(process.env.SKS_CODEX_PLUGIN_DETAIL_CONCURRENCY || 6) || 6);
27
+ let failed = 0;
28
+ const plugins = await mapWithConcurrency(summaries, concurrency, async (summary) => {
27
29
  const detail = await runCodexPluginDetailJson(summary.id || summary.name).catch((err) => ({ error: err?.message || String(err) }));
28
- plugins.push(normalizePlugin(summary, detail));
29
- }
30
+ if (detail?.error || normalizeList(detail?.blockers).length > 0)
31
+ failed += 1;
32
+ return normalizePlugin(summary, detail);
33
+ });
30
34
  const blockers = [
31
35
  ...(capability.supports_plugin_json ? [] : ['codex_0_138_plugin_json_unavailable']),
32
36
  ...normalizeList(listJson?.blockers)
@@ -35,11 +39,29 @@ export async function buildCodexPluginInventory() {
35
39
  schema: 'sks.codex-plugin-inventory.v1',
36
40
  generated_at: nowIso(),
37
41
  codex_0138_capability: capability,
42
+ fetch_concurrency: concurrency,
43
+ detail_fetch_count: summaries.length,
44
+ detail_fetch_failed_count: failed,
45
+ duration_ms: Date.now() - started,
38
46
  plugins,
39
47
  marketplace_available: plugins.some((plugin) => plugin.source === 'marketplace' || plugin.source === 'remote') || Boolean(listJson?.marketplace_available || listJson?.marketplaceAvailable),
40
48
  blockers
41
49
  };
42
50
  }
51
+ export async function mapWithConcurrency(items, concurrency, fn) {
52
+ const limit = Math.max(1, Math.floor(concurrency || 1));
53
+ const results = new Array(items.length);
54
+ let next = 0;
55
+ async function worker() {
56
+ while (next < items.length) {
57
+ const index = next;
58
+ next += 1;
59
+ results[index] = await fn(items[index]);
60
+ }
61
+ }
62
+ await Promise.all(Array.from({ length: Math.min(limit, items.length || 1) }, () => worker()));
63
+ return results;
64
+ }
43
65
  export async function writeCodexPluginInventoryArtifacts(root, inventory = null) {
44
66
  const report = inventory || await buildCodexPluginInventory();
45
67
  const artifact = path.join(root, '.sneakoscope', 'codex-plugin-inventory.json');
@@ -126,15 +148,17 @@ function boolish(value, fallback = false) {
126
148
  return fallback;
127
149
  }
128
150
  function fakePluginList() {
151
+ const count = Math.max(1, Number(process.env.SKS_CODEX_PLUGIN_JSON_FAKE_COUNT || 1) || 1);
129
152
  return {
130
153
  marketplace_available: true,
131
- plugins: [{
132
- id: 'fixture-plugin',
133
- name: 'Fixture Plugin',
134
- source: 'marketplace',
135
- installed: true,
136
- enabled: true
137
- }]
154
+ plugins: Array.from({ length: count }, (_, index) => ({
155
+ id: 'fixture-plugin',
156
+ name: index === 0 ? 'Fixture Plugin' : `Fixture Plugin ${index + 1}`,
157
+ ...(index === 0 ? {} : { id: `fixture-plugin-${index + 1}` }),
158
+ source: 'marketplace',
159
+ installed: true,
160
+ enabled: true
161
+ }))
138
162
  };
139
163
  }
140
164
  function fakePluginDetail(pluginId) {
@@ -21,6 +21,7 @@ import { diffCodexAppUiSnapshots, writeCodexAppUiSnapshot } from '../codex-app/c
21
21
  import { checkSksUpdateNotice } from '../update/update-notice.js';
22
22
  import { createMadDbCapability, MAD_DB_ACK } from '../mad-db/mad-db-capability.js';
23
23
  import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
24
+ import { writeCodex0139CapabilityArtifacts } from '../codex-control/codex-0139-capability.js';
24
25
  export async function madHighCommand(args = [], deps = {}) {
25
26
  const subcommand = firstSubcommand(args);
26
27
  if (subcommand)
@@ -37,6 +38,10 @@ export async function madHighCommand(args = [], deps = {}) {
37
38
  process.exitCode = 1;
38
39
  return;
39
40
  }
41
+ // Zellij is checked the same way Codex is, but it stays NON-blocking: a
42
+ // failed or skipped zellij upgrade never prevents the MAD launch.
43
+ const zellijUpdate = deps.maybePromptZellijUpdateForLaunch ? await deps.maybePromptZellijUpdateForLaunch(args, { label: 'MAD launch' }).catch(() => ({ status: 'error' })) : { status: 'skipped' };
44
+ void zellijUpdate;
40
45
  const depStatus = deps.ensureMadLaunchDependencies ? await deps.ensureMadLaunchDependencies(args) : { ready: true, actions: [] };
41
46
  if (!depStatus.ready) {
42
47
  console.error('SKS MAD launch blocked by missing dependencies.');
@@ -381,6 +386,7 @@ async function activateMadZellijPermissionState(cwd = process.cwd(), args = [])
381
386
  const dbWriteAllowed = has('db_write');
382
387
  const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: 'sks --mad Zellij scoped high-power maintenance session' });
383
388
  await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch(() => null);
389
+ await writeCodex0139CapabilityArtifacts(root, { missionId: id }).catch(() => null);
384
390
  const protectedCore = resolveProtectedCore({ packageRoot: packageRoot(), targetRoot: cwd });
385
391
  // The interactive launch 'before' snapshot is only persisted (env + policy json)
386
392
  // and is never compared against an 'after' snapshot during the session, so the
@@ -567,6 +573,7 @@ function codexLbImmediateLaunchOpts(args = [], lb = {}, opts = {}) {
567
573
  export async function madSksFixture(root) {
568
574
  const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: '$MAD-SKS fixture permission gate' });
569
575
  await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch(() => null);
576
+ await writeCodex0139CapabilityArtifacts(root, { missionId: id }).catch(() => null);
570
577
  const gate = { schema_version: 1, passed: true, mad_sks_permission_active: true, permissions_deactivated: true, catastrophic_safety_guard_active: true, permission_profile: permissionGateSummary(), fixture: true };
571
578
  await writeJsonAtomic(path.join(dir, 'mad-sks-gate.json'), gate);
572
579
  return { mission_id: id, dir, gate };
@@ -742,6 +749,7 @@ async function materializeMadSksRun(root, targetRoot, permission, userIntent, js
742
749
  await initProject(root, {});
743
750
  const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: userIntent });
744
751
  await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch(() => null);
752
+ await writeCodex0139CapabilityArtifacts(root, { missionId: id }).catch(() => null);
745
753
  const before = await snapshotProtectedCore(packageRoot(), 'before');
746
754
  const authorization = opts.authorizationManifest || createMadSksAuthorizationManifest({ permission, userIntent });
747
755
  const authorizationPath = opts.authorizationManifestPath || path.join(dir, 'mad-sks-authorization.json');
@@ -7,6 +7,7 @@ import { buildNarutoCloneRoster, systemSafeNarutoConcurrency } from '../agents/a
7
7
  import { DEFAULT_NARUTO_CLONES, MAX_NARUTO_AGENT_COUNT } from '../agents/agent-schema.js';
8
8
  import { resolveOllamaWorkerConfig } from '../agents/ollama-worker-config.js';
9
9
  import { attachZellijSessionInteractive, launchZellijLayout } from '../zellij/zellij-launcher.js';
10
+ import { maybePromptZellijUpdateForLaunch } from '../zellij/zellij-update.js';
10
11
  import { buildNarutoWorkGraph } from '../naruto/naruto-work-graph.js';
11
12
  import { buildNarutoRoleDistribution } from '../naruto/naruto-role-policy.js';
12
13
  import { decideNarutoConcurrency } from '../naruto/naruto-concurrency-governor.js';
@@ -15,12 +16,14 @@ import { collectActualNarutoWorker, spawnActualNarutoWorker } from '../naruto/na
15
16
  import { allocateNarutoTasksToWorkers } from '../naruto/naruto-allocation-policy.js';
16
17
  import { rebalanceNarutoReadyWork } from '../naruto/naruto-rebalance-policy.js';
17
18
  import { buildNarutoVerificationDag } from '../naruto/naruto-verification-dag.js';
19
+ import { evaluateNarutoFinalizer } from '../naruto/naruto-finalizer.js';
18
20
  import { buildNarutoGptFinalPack } from '../naruto/naruto-gpt-final-pack.js';
19
21
  import { planNarutoZellijDashboard } from '../zellij/zellij-naruto-dashboard.js';
20
22
  import { checkPromptPlaceholders } from '../prompt/prompt-placeholder-guard.js';
21
23
  import { evaluateGitWorktreeCapability } from '../git/git-worktree-capability.js';
22
24
  import { buildRuntimeProofSummary, renderRuntimeProofSummary } from '../agents/runtime-proof-summary.js';
23
25
  import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
26
+ import { writeCodex0139CapabilityArtifacts } from '../codex-control/codex-0139-capability.js';
24
27
  const NARUTO_RESULT_SCHEMA = 'sks.naruto-command-result.v1';
25
28
  const NARUTO_ROUTE = '$Naruto';
26
29
  // $Naruto — Shadow Clone Swarm (影分身 / Kage Bunshin no Jutsu).
@@ -41,6 +44,12 @@ export async function narutoCommand(commandOrArgs = 'naruto', maybeArgs = []) {
41
44
  return narutoWorkers(parsed);
42
45
  if (parsed.action === 'proof')
43
46
  return narutoProof(parsed);
47
+ // Like the Codex CLI update prompt: check the installed zellij version and
48
+ // offer an upgrade to the latest stable release before the live session
49
+ // opens. Never blocks the run.
50
+ if (!parsed.json && !parsed.mock && !parsed.noOpenZellij) {
51
+ await maybePromptZellijUpdateForLaunch(args, { label: '$Naruto launch' }).catch(() => undefined);
52
+ }
44
53
  return narutoRun(parsed);
45
54
  }
46
55
  async function narutoRun(parsed) {
@@ -75,6 +84,7 @@ async function narutoRun(parsed) {
75
84
  });
76
85
  const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
77
86
  await writeCodex0138CapabilityArtifacts(root, { missionId: mission.id }).catch(() => null);
87
+ await writeCodex0139CapabilityArtifacts(root, { missionId: mission.id }).catch(() => null);
78
88
  const gitWorktreeCapability = writeCapable
79
89
  ? await evaluateGitWorktreeCapability({ root, missionId: mission.id })
80
90
  : null;
@@ -299,6 +309,10 @@ async function narutoRun(parsed) {
299
309
  console.log(' parallelism mode: ' + parsed.parallelism);
300
310
  if (activeSlots < roster.agent_count)
301
311
  console.log(' cap reasons: ' + (governor.reasons.join(', ') || 'host safety cap'));
312
+ // Backpressure used to throttle silently (50% when throttled, 25% when
313
+ // saturated); always tell the operator when host pressure reduced workers.
314
+ if (governor.backpressure !== 'normal')
315
+ console.log(' backpressure: ' + governor.backpressure + ' — host resource pressure reduced active workers (memory/cpu/fd/disk thresholds)');
302
316
  if (parsed.parallelism !== 'safe' && activeSlots < 10)
303
317
  console.log(' warning: active workers below 10 in non-safe mode');
304
318
  }
@@ -418,6 +432,18 @@ async function narutoRun(parsed) {
418
432
  });
419
433
  const clones = result.roster?.agent_count ?? roster.agent_count;
420
434
  const localWorkerSummary = summarizeNarutoLocalWorkerResult(localWorker, result);
435
+ // Finalizer policy: when local LLM workers contributed patches, the GPT
436
+ // final arbiter must have accepted before patches are considered final.
437
+ const finalizer = evaluateNarutoFinalizer({
438
+ localParticipated: Number(localWorkerSummary?.selected_worker_count || 0) > 0,
439
+ gptFinalStatus: result.proof?.gpt_final_status || null,
440
+ applyPatches: writeCapable
441
+ });
442
+ await writeJsonAtomic(path.join(mission.dir, 'naruto-finalizer.json'), {
443
+ ...finalizer,
444
+ generated_at: nowIso(),
445
+ mission_id: mission.id
446
+ });
421
447
  const summary = {
422
448
  schema: NARUTO_RESULT_SCHEMA,
423
449
  ok: result.ok === true,
@@ -477,6 +503,7 @@ async function narutoRun(parsed) {
477
503
  passed: parallelRuntime.passed
478
504
  } : null,
479
505
  local_worker: localWorkerSummary,
506
+ finalizer,
480
507
  proof: result.proof?.status || 'missing',
481
508
  run: compactNarutoRunResult(result),
482
509
  zellij: null
@@ -489,6 +516,8 @@ async function narutoRun(parsed) {
489
516
  console.log('Backend: ' + result.backend);
490
517
  console.log('Roles: ' + roleDistribution.entries.map((entry) => `${entry.role}:${entry.count}`).join(', '));
491
518
  console.log('Proof: ' + summary.proof);
519
+ if (!finalizer.ok)
520
+ console.log('Finalizer: blocked — ' + finalizer.blockers.join(', '));
492
521
  if (summary.parallel_runtime) {
493
522
  console.log('$Naruto parallel proof:');
494
523
  console.log(' max active workers: ' + summary.parallel_runtime.max_observed_active_workers);
@@ -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.1';
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
@@ -1249,13 +1249,15 @@ function subagentToolName(payload) {
1249
1249
  }
1250
1250
  function subagentStage(payload) {
1251
1251
  const hay = JSON.stringify(payload || {});
1252
- if (!/(spawn_agent|send_input|wait_agent|close_agent|subagent|worker|explorer)/i.test(hay))
1252
+ // Codex 0.139 renamed multi-agent v2 `close_agent` to `interrupt_agent`;
1253
+ // accept both so cockpit evidence keeps classifying on newer CLIs.
1254
+ if (!/(spawn_agent|send_input|wait_agent|close_agent|interrupt_agent|subagent|worker|explorer)/i.test(hay))
1253
1255
  return null;
1254
1256
  if (/subagent[_ -]?unavailable|subagents unavailable|unsafe to split|unsplittable|cannot safely split/i.test(hay))
1255
1257
  return 'exception';
1256
1258
  if (/spawn_agent/i.test(hay))
1257
1259
  return 'spawn_agent';
1258
- if (/wait_agent|close_agent|completed|final/i.test(hay))
1260
+ if (/wait_agent|close_agent|interrupt_agent|completed|final/i.test(hay))
1259
1261
  return 'result';
1260
1262
  return 'subagent';
1261
1263
  }
@@ -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 = [