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.
- package/README.md +135 -90
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/commands/doctor.js +39 -1
- package/dist/commands/mad-sks.js +2 -0
- package/dist/commands/zellij.js +58 -1
- package/dist/core/agents/agent-effort-policy.js +7 -1
- package/dist/core/agents/agent-scheduler.js +32 -24
- package/dist/core/agents/native-cli-session-swarm.js +22 -2
- package/dist/core/codex-app/codex-app-handoff.js +98 -0
- package/dist/core/codex-app/codex-app-launcher.js +103 -0
- package/dist/core/codex-control/codex-0138-capability.js +102 -0
- package/dist/core/codex-control/codex-model-capabilities.js +62 -0
- package/dist/core/codex-control/codex-model-metadata.js +91 -0
- package/dist/core/codex-control/codex-sdk-config-policy.js +1 -1
- package/dist/core/codex-control/codex-task-runner.js +1 -1
- package/dist/core/codex-plugins/codex-plugin-cache.js +38 -0
- package/dist/core/codex-plugins/codex-plugin-diff.js +73 -0
- package/dist/core/codex-plugins/codex-plugin-json.js +176 -0
- package/dist/core/commands/mad-sks-command.js +8 -0
- package/dist/core/commands/naruto-command.js +30 -1
- package/dist/core/commands/qa-loop-command.js +147 -5
- package/dist/core/doctor/codex-0138-doctor.js +104 -0
- package/dist/core/doctor/doctor-readiness-matrix.js +11 -0
- package/dist/core/effort-orchestrator.js +9 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/hooks-runtime.js +6 -9
- package/dist/core/image/image-artifact-path-contract.js +101 -0
- package/dist/core/image/image-artifact-registry.js +33 -0
- package/dist/core/image-ux-review/imagegen-adapter.js +49 -17
- package/dist/core/mad-db/mad-db-result-lifecycle.js +71 -0
- package/dist/core/mcp/mcp-plugin-inventory.js +29 -0
- package/dist/core/mcp/mcp-server-policy.js +24 -0
- package/dist/core/qa-loop/qa-loop-app-handoff-confirmation.js +51 -0
- package/dist/core/qa-loop/qa-loop-budget-policy.js +37 -0
- package/dist/core/qa-loop.js +70 -3
- package/dist/core/release/release-gate-cache-v2.js +47 -5
- package/dist/core/usage/codex-account-usage.js +139 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-slot-column-anchor.js +16 -7
- package/dist/core/zellij/zellij-slot-pane-renderer.js +23 -2
- package/dist/core/zellij/zellij-slot-telemetry.js +65 -12
- package/dist/core/zellij/zellij-ui-mode.js +8 -1
- package/dist/core/zellij/zellij-update.js +307 -0
- package/dist/core/zellij/zellij-worker-pane-manager.js +211 -145
- package/dist/scripts/release-gate-existence-audit.js +5 -1
- package/package.json +46 -3
- package/schemas/codex-app/codex-app-handoff.schema.json +20 -0
- package/schemas/codex-plugins/codex-plugin-inventory.schema.json +32 -0
- package/schemas/image/image-artifact-path-contract.schema.json +32 -0
- package/schemas/usage/codex-account-usage.schema.json +27 -0
- package/dist/core/naruto/naruto-work-stealing.js +0 -11
- 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
|
package/dist/core/qa-loop.js
CHANGED
|
@@ -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'), {
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '
|
|
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
|
|
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
|
-
|
|
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(-
|
|
260
|
+
.slice(-7)
|
|
240
261
|
.map((line) => `live: ${trimInline(line, 72)}`);
|
|
241
262
|
}
|
|
242
263
|
function findTelemetrySlot(snapshot, slotId, generationIndex) {
|