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.
- package/README.md +127 -71
- 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/mad-sks.js +2 -0
- package/dist/commands/zellij.js +58 -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 +30 -9
- package/dist/core/codex-app/codex-app-launcher.js +103 -0
- package/dist/core/codex-control/codex-0138-capability.js +42 -4
- package/dist/core/codex-control/codex-0139-capability.js +102 -0
- package/dist/core/codex-control/codex-model-capabilities.js +25 -4
- package/dist/core/codex-control/codex-model-metadata.js +91 -0
- 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 +35 -11
- package/dist/core/commands/mad-sks-command.js +8 -0
- package/dist/core/commands/naruto-command.js +29 -0
- package/dist/core/commands/qa-loop-command.js +41 -6
- package/dist/core/fsx.js +1 -1
- package/dist/core/image/image-artifact-path-contract.js +2 -0
- package/dist/core/image/image-artifact-registry.js +33 -0
- package/dist/core/image-ux-review/imagegen-adapter.js +27 -16
- package/dist/core/pipeline-internals/runtime-core.js +4 -2
- package/dist/core/qa-loop/qa-loop-app-handoff-confirmation.js +51 -0
- package/dist/core/qa-loop/qa-loop-budget-policy.js +1 -1
- package/dist/core/qa-loop.js +44 -3
- package/dist/core/release/release-gate-cache-v2.js +47 -5
- package/dist/core/usage/codex-account-usage.js +77 -16
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-slot-pane-renderer.js +5 -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/package.json +23 -2
- package/dist/core/naruto/naruto-work-stealing.js +0 -11
- 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
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 = '
|
|
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
|
-
|
|
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
|
|
504
|
-
//
|
|
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
|
-
|
|
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:
|
|
12
|
+
ok: available,
|
|
13
13
|
account_usage_source: usage?.source || 'unavailable',
|
|
14
14
|
token_usage_available: available,
|
|
15
15
|
near_limit: nearLimit,
|
package/dist/core/qa-loop.js
CHANGED
|
@@ -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 (
|
|
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'), {
|
|
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 = [
|