sneakoscope 2.0.17 → 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 (56) hide show
  1. package/README.md +135 -90
  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/commands/mad-sks.js +2 -0
  9. package/dist/commands/zellij.js +58 -1
  10. package/dist/core/agents/agent-effort-policy.js +7 -1
  11. package/dist/core/agents/agent-scheduler.js +32 -24
  12. package/dist/core/agents/native-cli-session-swarm.js +22 -2
  13. package/dist/core/codex-app/codex-app-handoff.js +98 -0
  14. package/dist/core/codex-app/codex-app-launcher.js +103 -0
  15. package/dist/core/codex-control/codex-0138-capability.js +102 -0
  16. package/dist/core/codex-control/codex-model-capabilities.js +62 -0
  17. package/dist/core/codex-control/codex-model-metadata.js +91 -0
  18. package/dist/core/codex-control/codex-sdk-config-policy.js +1 -1
  19. package/dist/core/codex-control/codex-task-runner.js +1 -1
  20. package/dist/core/codex-plugins/codex-plugin-cache.js +38 -0
  21. package/dist/core/codex-plugins/codex-plugin-diff.js +73 -0
  22. package/dist/core/codex-plugins/codex-plugin-json.js +176 -0
  23. package/dist/core/commands/mad-sks-command.js +8 -0
  24. package/dist/core/commands/naruto-command.js +30 -1
  25. package/dist/core/commands/qa-loop-command.js +147 -5
  26. package/dist/core/doctor/codex-0138-doctor.js +104 -0
  27. package/dist/core/doctor/doctor-readiness-matrix.js +11 -0
  28. package/dist/core/effort-orchestrator.js +9 -0
  29. package/dist/core/fsx.js +1 -1
  30. package/dist/core/hooks-runtime.js +6 -9
  31. package/dist/core/image/image-artifact-path-contract.js +101 -0
  32. package/dist/core/image/image-artifact-registry.js +33 -0
  33. package/dist/core/image-ux-review/imagegen-adapter.js +49 -17
  34. package/dist/core/mad-db/mad-db-result-lifecycle.js +71 -0
  35. package/dist/core/mcp/mcp-plugin-inventory.js +29 -0
  36. package/dist/core/mcp/mcp-server-policy.js +24 -0
  37. package/dist/core/qa-loop/qa-loop-app-handoff-confirmation.js +51 -0
  38. package/dist/core/qa-loop/qa-loop-budget-policy.js +37 -0
  39. package/dist/core/qa-loop.js +70 -3
  40. package/dist/core/release/release-gate-cache-v2.js +47 -5
  41. package/dist/core/usage/codex-account-usage.js +139 -0
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/zellij/zellij-slot-column-anchor.js +16 -7
  44. package/dist/core/zellij/zellij-slot-pane-renderer.js +23 -2
  45. package/dist/core/zellij/zellij-slot-telemetry.js +65 -12
  46. package/dist/core/zellij/zellij-ui-mode.js +8 -1
  47. package/dist/core/zellij/zellij-update.js +307 -0
  48. package/dist/core/zellij/zellij-worker-pane-manager.js +211 -145
  49. package/dist/scripts/release-gate-existence-audit.js +5 -1
  50. package/package.json +46 -3
  51. package/schemas/codex-app/codex-app-handoff.schema.json +20 -0
  52. package/schemas/codex-plugins/codex-plugin-inventory.schema.json +32 -0
  53. package/schemas/image/image-artifact-path-contract.schema.json +32 -0
  54. package/schemas/usage/codex-account-usage.schema.json +27 -0
  55. package/dist/core/naruto/naruto-work-stealing.js +0 -11
  56. package/dist/core/zellij/zellij-right-column-layout-proof.js +0 -42
@@ -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
@@ -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: available,
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,17 @@ 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_confirmed: false,
317
+ desktop_app_handoff_verdict: null,
318
+ desktop_app_handoff_confirmation_artifact: null,
319
+ desktop_app_handoff_is_web_ui_evidence: false,
320
+ image_artifact_path_contract_present: false,
321
+ image_artifact_path_contract_artifact: null,
322
+ image_artifact_path_contract_blockers: [],
312
323
  api_e2e_required: apiRequired,
313
324
  unsafe_external_side_effects: false,
314
325
  corrective_loop_enabled: corrective,
@@ -374,6 +385,21 @@ export async function evaluateQaGate(dir) {
374
385
  if (evidenceMentionsForbiddenWebComputerUseEvidence({ evidence: gate.evidence, ui_evidence_source: gate.ui_evidence_source }))
375
386
  reasons.push('computer_use_web_evidence_forbidden');
376
387
  }
388
+ if (gate.desktop_app_handoff_required === true) {
389
+ if (!['pending', 'launched_pending_confirmation', 'completed'].includes(String(gate.desktop_app_handoff_status || '')))
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');
397
+ if (gate.desktop_app_handoff_is_web_ui_evidence === true)
398
+ reasons.push('desktop_app_handoff_misused_as_web_evidence');
399
+ }
400
+ const imageBlockers = Array.isArray(gate.image_artifact_path_contract_blockers) ? gate.image_artifact_path_contract_blockers : [];
401
+ if (imageBlockers.includes('image_generated_file_path_missing'))
402
+ reasons.push('image_generated_file_path_missing');
377
403
  if (!reportFile)
378
404
  reasons.push('qa_report_file_missing');
379
405
  else if (!isQaReportFilename(reportFile))
@@ -393,11 +419,48 @@ export async function writeMockQaResult(dir, mission, contract) {
393
419
  const reportFile = isQaReportFilename(previousReportFile) ? previousReportFile : qaReportFilename();
394
420
  const uiRequired = qaUiRequired(contract.answers || {});
395
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`);
396
- 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
+ });
397
454
  return evaluateQaGate(dir);
398
455
  }
399
- export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile }) {
456
+ export function buildQaLoopPrompt({ id, mission, contract, cycle, previous, reportFile, imagePathContract, appHandoff }) {
400
457
  const report = reportFile && isQaReportFilename(reportFile) ? reportFile : 'the date/version-prefixed report named by qa-gate.json.qa_report_file';
458
+ const imageContractText = imagePathContract
459
+ ? `\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`
460
+ : '';
461
+ const appHandoffText = appHandoff
462
+ ? `\nCODEX DESKTOP /app HANDOFF:\n${JSON.stringify(appHandoff, null, 2)}\nThis is desktop-app review status only and is not web UI evidence.\n`
463
+ : '';
401
464
  return `SKS QA-LOOP
402
465
  MISSION: ${id}
403
466
  TASK: ${mission.prompt}
@@ -410,6 +473,7 @@ GATE: passed=false while unresolved_findings or unresolved_fixable_findings > 0,
410
473
  ARTIFACTS: update qa-ledger.json, ${report}, qa-gate.json, and qa-loop/cycle-${cycle}/.
411
474
  CONTRACT:
412
475
  ${JSON.stringify(contract, null, 2)}
476
+ ${imageContractText}${appHandoffText}
413
477
  Previous tail:
414
478
  ${String(previous || '').slice(-2500)}
415
479
  `;
@@ -417,9 +481,12 @@ ${String(previous || '').slice(-2500)}
417
481
  export async function qaStatus(dir) {
418
482
  const gate = await readJson(path.join(dir, 'qa-gate.evaluated.json'), await readJson(path.join(dir, 'qa-gate.json'), null));
419
483
  const ledger = await readJson(path.join(dir, 'qa-ledger.json'), null);
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);
486
+ const imagePathContract = await readJson(path.join(dir, 'qa-loop', 'image-artifact-path-contract.json'), null);
420
487
  const reportFile = qaReportFileFromGate(gate?.gate || gate || {}) || ledger?.qa_report_file || null;
421
488
  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()) };
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 };
423
490
  }
424
491
  function qaChecklist(a) {
425
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)) {
@@ -0,0 +1,139 @@
1
+ import path from 'node:path';
2
+ import { findCodexBinary } from '../codex-adapter.js';
3
+ import { nowIso, runProcess, writeJsonAtomic } from '../fsx.js';
4
+ export async function collectCodexAccountUsage() {
5
+ if (process.env.SKS_CODEX_ACCOUNT_USAGE_FAKE === '1') {
6
+ return {
7
+ schema: 'sks.codex-account-usage.v1',
8
+ generated_at: nowIso(),
9
+ ok: true,
10
+ source: 'fake',
11
+ account_id: 'fake-account',
12
+ token_usage: {
13
+ input_tokens: 1000,
14
+ output_tokens: 500,
15
+ total_tokens: 1500,
16
+ reset_at: null
17
+ },
18
+ usage_limit_tokens: 100000,
19
+ attempted_sources: ['fake'],
20
+ blockers: []
21
+ };
22
+ }
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
+ }
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);
55
+ }
56
+ export async function writeCodexAccountUsageArtifacts(root, input = {}) {
57
+ const snapshot = await collectCodexAccountUsage();
58
+ const rootArtifact = path.join(root, '.sneakoscope', 'codex-account-usage.json');
59
+ await writeJsonAtomic(rootArtifact, snapshot);
60
+ let missionArtifact = null;
61
+ if (input.missionId) {
62
+ missionArtifact = path.join(root, '.sneakoscope', 'missions', input.missionId, 'codex-account-usage.json');
63
+ await writeJsonAtomic(missionArtifact, snapshot);
64
+ }
65
+ return { snapshot, root_artifact: rootArtifact, mission_artifact: missionArtifact };
66
+ }
67
+ function normalizeUsagePayload(payload, source, attemptedSources) {
68
+ const usage = payload?.token_usage || payload?.usage || payload;
69
+ const input = Number(usage?.input_tokens || usage?.inputTokens || 0);
70
+ const output = Number(usage?.output_tokens || usage?.outputTokens || 0);
71
+ const total = Number(usage?.total_tokens || usage?.totalTokens || input + output);
72
+ return {
73
+ schema: 'sks.codex-account-usage.v1',
74
+ generated_at: nowIso(),
75
+ ok: true,
76
+ source,
77
+ account_id: payload?.account_id || payload?.accountId || null,
78
+ token_usage: {
79
+ input_tokens: Number.isFinite(input) ? input : 0,
80
+ output_tokens: Number.isFinite(output) ? output : 0,
81
+ total_tokens: Number.isFinite(total) ? total : 0,
82
+ reset_at: usage?.reset_at || usage?.resetAt || null
83
+ },
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,
86
+ blockers: []
87
+ };
88
+ }
89
+ function unavailable(blockers, attemptedSources = []) {
90
+ return {
91
+ schema: 'sks.codex-account-usage.v1',
92
+ generated_at: nowIso(),
93
+ ok: false,
94
+ source: 'unavailable',
95
+ token_usage: null,
96
+ usage_limit_tokens: null,
97
+ attempted_sources: attemptedSources,
98
+ blockers
99
+ };
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
+ }
139
+ //# sourceMappingURL=codex-account-usage.js.map
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '2.0.17';
1
+ export const PACKAGE_VERSION = '3.0.0';
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)
@@ -197,7 +215,10 @@ async function tryRenderTelemetrySlotPane(input) {
197
215
  return null;
198
216
  const staleRows = staleTelemetryRows(telemetryStatus(snapshot).telemetry_age_ms);
199
217
  const fallbackRows = artifactFallbackRows(input.artifactRender);
200
- const liveRows = staleRows.length || !slot.progress ? fallbackRows : [];
218
+ // Always surface the live artifact rows (current file, tool events, stdout
219
+ // tail). Telemetry freshness only tells us the worker is alive — the user
220
+ // still needs to see WHAT the worker is doing right now.
221
+ const liveRows = fallbackRows;
201
222
  if (slot.status === 'failed') {
202
223
  return [
203
224
  `${slot.slot_id} gen-${slot.generation_index} · FAILED`,
@@ -236,7 +257,7 @@ function artifactFallbackRows(text) {
236
257
  .map((line) => line.replace(/^\|\s?/, '').replace(/\s?\|$/, '').trim())
237
258
  .filter((line) => /^(heartbeat|doing|files|event|out|err):\s+/i.test(line))
238
259
  .filter((line) => !/unknown|waiting for worker intake|no changed file yet/i.test(line))
239
- .slice(-4)
260
+ .slice(-7)
240
261
  .map((line) => `live: ${trimInline(line, 72)}`);
241
262
  }
242
263
  function findTelemetrySlot(snapshot, slotId, generationIndex) {