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,38 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readJson, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { buildCodexPluginInventory } from './codex-plugin-json.js';
|
|
4
|
+
export function codexPluginInventoryCachePath(root) {
|
|
5
|
+
return path.join(root, '.sneakoscope', 'cache', 'codex-plugin-inventory.json');
|
|
6
|
+
}
|
|
7
|
+
export async function readCodexPluginInventoryCache(root) {
|
|
8
|
+
const cache = await readJson(codexPluginInventoryCachePath(root), null);
|
|
9
|
+
return cache?.schema === 'sks.codex-plugin-inventory-cache.v1' ? cache : null;
|
|
10
|
+
}
|
|
11
|
+
export async function writeCodexPluginInventoryCache(root, inventory, ttlMs = defaultTtlMs()) {
|
|
12
|
+
const generatedAt = new Date();
|
|
13
|
+
const cache = {
|
|
14
|
+
schema: 'sks.codex-plugin-inventory-cache.v1',
|
|
15
|
+
generated_at: generatedAt.toISOString(),
|
|
16
|
+
expires_at: new Date(generatedAt.getTime() + ttlMs).toISOString(),
|
|
17
|
+
ttl_ms: ttlMs,
|
|
18
|
+
inventory
|
|
19
|
+
};
|
|
20
|
+
await writeJsonAtomic(codexPluginInventoryCachePath(root), cache);
|
|
21
|
+
return cache;
|
|
22
|
+
}
|
|
23
|
+
export async function getCodexPluginInventoryCached(root, opts = {}) {
|
|
24
|
+
const ttlMs = Math.max(1, Number(opts.ttlMs || defaultTtlMs()) || defaultTtlMs());
|
|
25
|
+
const cachePath = codexPluginInventoryCachePath(root);
|
|
26
|
+
const existing = opts.forceRefresh ? null : await readCodexPluginInventoryCache(root);
|
|
27
|
+
if (existing && Date.parse(existing.expires_at) > Date.now()) {
|
|
28
|
+
return { inventory: existing.inventory, cache_hit: true, cache_path: cachePath, cache: existing };
|
|
29
|
+
}
|
|
30
|
+
const inventory = await (opts.inventoryFactory || buildCodexPluginInventory)();
|
|
31
|
+
const cache = await writeCodexPluginInventoryCache(root, inventory, ttlMs);
|
|
32
|
+
return { inventory, cache_hit: false, cache_path: cachePath, cache };
|
|
33
|
+
}
|
|
34
|
+
function defaultTtlMs() {
|
|
35
|
+
const value = Number(process.env.SKS_CODEX_PLUGIN_CACHE_TTL_MS || 10 * 60 * 1000);
|
|
36
|
+
return Number.isFinite(value) && value > 0 ? value : 10 * 60 * 1000;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=codex-plugin-cache.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
export function diffCodexPluginInventories(previous, current) {
|
|
4
|
+
const prev = pluginMap(previous);
|
|
5
|
+
const next = pluginMap(current);
|
|
6
|
+
const added = [...next.keys()].filter((id) => !prev.has(id)).sort();
|
|
7
|
+
const removed = [...prev.keys()].filter((id) => !next.has(id)).sort();
|
|
8
|
+
const shared = [...next.keys()].filter((id) => prev.has(id));
|
|
9
|
+
const changedRemote = [];
|
|
10
|
+
const changedTemplates = [];
|
|
11
|
+
const changedPrompts = [];
|
|
12
|
+
const changedMetadata = [];
|
|
13
|
+
for (const id of shared) {
|
|
14
|
+
const before = prev.get(id);
|
|
15
|
+
const after = next.get(id);
|
|
16
|
+
if (!sameJson(normalizePluginMetadata(before), normalizePluginMetadata(after)))
|
|
17
|
+
changedMetadata.push(id);
|
|
18
|
+
if (!sameJson(normalizeRemoteServers(before?.remote_mcp_servers), normalizeRemoteServers(after?.remote_mcp_servers)))
|
|
19
|
+
changedRemote.push(id);
|
|
20
|
+
if (!sameJson(sorted(before?.unavailable_app_templates), sorted(after?.unavailable_app_templates)))
|
|
21
|
+
changedTemplates.push(id);
|
|
22
|
+
if (!sameJson(sorted(before?.default_prompts), sorted(after?.default_prompts)))
|
|
23
|
+
changedPrompts.push(id);
|
|
24
|
+
}
|
|
25
|
+
const changedCount = added.length + removed.length + changedMetadata.length + changedRemote.length + changedTemplates.length + changedPrompts.length;
|
|
26
|
+
return {
|
|
27
|
+
schema: 'sks.codex-plugin-inventory-diff.v1',
|
|
28
|
+
generated_at: new Date().toISOString(),
|
|
29
|
+
ok: true,
|
|
30
|
+
added_plugins: added,
|
|
31
|
+
removed_plugins: removed,
|
|
32
|
+
changed_remote_mcp_servers: changedRemote.sort(),
|
|
33
|
+
changed_unavailable_app_templates: changedTemplates.sort(),
|
|
34
|
+
changed_default_prompts: changedPrompts.sort(),
|
|
35
|
+
changed_plugin_metadata: changedMetadata.sort(),
|
|
36
|
+
changed_count: changedCount
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function writeCodexPluginInventoryDiff(root, previous, current) {
|
|
40
|
+
const diff = diffCodexPluginInventories(previous, current);
|
|
41
|
+
const artifact = path.join(root, '.sneakoscope', 'codex-plugin-inventory.diff.json');
|
|
42
|
+
await writeJsonAtomic(artifact, diff);
|
|
43
|
+
return { diff, artifact };
|
|
44
|
+
}
|
|
45
|
+
function pluginMap(inventory) {
|
|
46
|
+
const map = new Map();
|
|
47
|
+
for (const plugin of inventory?.plugins || [])
|
|
48
|
+
map.set(String(plugin.id || plugin.name), plugin);
|
|
49
|
+
return map;
|
|
50
|
+
}
|
|
51
|
+
function normalizeRemoteServers(rows) {
|
|
52
|
+
return (rows || []).map((row) => ({
|
|
53
|
+
name: row.name,
|
|
54
|
+
url: row.url,
|
|
55
|
+
auth_type: row.auth_type
|
|
56
|
+
})).sort((a, b) => `${a.name}:${a.url}:${a.auth_type}`.localeCompare(`${b.name}:${b.url}:${b.auth_type}`));
|
|
57
|
+
}
|
|
58
|
+
function normalizePluginMetadata(row) {
|
|
59
|
+
return {
|
|
60
|
+
id: row?.id || null,
|
|
61
|
+
name: row?.name || null,
|
|
62
|
+
source: row?.source || null,
|
|
63
|
+
installed: row?.installed === true,
|
|
64
|
+
enabled: row?.enabled === true
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function sorted(rows) {
|
|
68
|
+
return [...(rows || [])].map(String).sort();
|
|
69
|
+
}
|
|
70
|
+
function sameJson(a, b) {
|
|
71
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=codex-plugin-diff.js.map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { findCodexBinary } from '../codex-adapter.js';
|
|
3
|
+
import { detectCodex0138Capability } from '../codex-control/codex-0138-capability.js';
|
|
4
|
+
import { nowIso, runProcess, writeJsonAtomic } from '../fsx.js';
|
|
5
|
+
export async function runCodexPluginListJson() {
|
|
6
|
+
if (process.env.SKS_CODEX_PLUGIN_JSON_FAKE === '1')
|
|
7
|
+
return fakePluginList();
|
|
8
|
+
const bin = await findCodexBinary();
|
|
9
|
+
if (!bin)
|
|
10
|
+
return { plugins: [], blockers: ['codex_cli_missing'] };
|
|
11
|
+
return runCodexJson(bin, ['plugin', 'list', '--json']);
|
|
12
|
+
}
|
|
13
|
+
export async function runCodexPluginDetailJson(pluginId) {
|
|
14
|
+
if (process.env.SKS_CODEX_PLUGIN_JSON_FAKE === '1')
|
|
15
|
+
return fakePluginDetail(pluginId);
|
|
16
|
+
const bin = await findCodexBinary();
|
|
17
|
+
if (!bin)
|
|
18
|
+
return { blockers: ['codex_cli_missing'] };
|
|
19
|
+
return runCodexJson(bin, ['plugin', 'detail', pluginId, '--json']);
|
|
20
|
+
}
|
|
21
|
+
export async function buildCodexPluginInventory() {
|
|
22
|
+
const started = Date.now();
|
|
23
|
+
const capability = await detectCodex0138Capability();
|
|
24
|
+
const listJson = await runCodexPluginListJson();
|
|
25
|
+
const summaries = normalizePluginList(listJson);
|
|
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) => {
|
|
29
|
+
const detail = await runCodexPluginDetailJson(summary.id || summary.name).catch((err) => ({ error: err?.message || String(err) }));
|
|
30
|
+
if (detail?.error || normalizeList(detail?.blockers).length > 0)
|
|
31
|
+
failed += 1;
|
|
32
|
+
return normalizePlugin(summary, detail);
|
|
33
|
+
});
|
|
34
|
+
const blockers = [
|
|
35
|
+
...(capability.supports_plugin_json ? [] : ['codex_0_138_plugin_json_unavailable']),
|
|
36
|
+
...normalizeList(listJson?.blockers)
|
|
37
|
+
];
|
|
38
|
+
return {
|
|
39
|
+
schema: 'sks.codex-plugin-inventory.v1',
|
|
40
|
+
generated_at: nowIso(),
|
|
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,
|
|
46
|
+
plugins,
|
|
47
|
+
marketplace_available: plugins.some((plugin) => plugin.source === 'marketplace' || plugin.source === 'remote') || Boolean(listJson?.marketplace_available || listJson?.marketplaceAvailable),
|
|
48
|
+
blockers
|
|
49
|
+
};
|
|
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
|
+
}
|
|
65
|
+
export async function writeCodexPluginInventoryArtifacts(root, inventory = null) {
|
|
66
|
+
const report = inventory || await buildCodexPluginInventory();
|
|
67
|
+
const artifact = path.join(root, '.sneakoscope', 'codex-plugin-inventory.json');
|
|
68
|
+
await writeJsonAtomic(artifact, report);
|
|
69
|
+
return { report, artifact };
|
|
70
|
+
}
|
|
71
|
+
export function pluginAppTemplatePolicy(inventory) {
|
|
72
|
+
const unavailable = inventory.plugins.flatMap((plugin) => plugin.unavailable_app_templates.map((template) => ({
|
|
73
|
+
plugin: plugin.id,
|
|
74
|
+
template
|
|
75
|
+
})));
|
|
76
|
+
return {
|
|
77
|
+
schema: 'sks.codex-plugin-app-template-policy.v1',
|
|
78
|
+
ok: true,
|
|
79
|
+
unavailable_app_templates: unavailable,
|
|
80
|
+
qa_loop_app_handoff_recommended: unavailable.length > 0,
|
|
81
|
+
doctor_warnings: unavailable.map((row) => `plugin_app_template_unavailable:${row.plugin}`)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function runCodexJson(bin, args) {
|
|
85
|
+
const result = await runProcess(bin, args, { timeoutMs: 20_000, maxOutputBytes: 256 * 1024 }).catch((err) => ({
|
|
86
|
+
code: 1,
|
|
87
|
+
stdout: '',
|
|
88
|
+
stderr: err?.message || String(err)
|
|
89
|
+
}));
|
|
90
|
+
const text = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
91
|
+
try {
|
|
92
|
+
return text ? JSON.parse(text) : {};
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return { raw_text: text, blockers: [`codex_plugin_json_parse_failed:${args.join(' ')}`] };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function normalizePluginList(value) {
|
|
99
|
+
if (Array.isArray(value))
|
|
100
|
+
return value;
|
|
101
|
+
for (const key of ['plugins', 'installed_plugins', 'installedPlugins', 'items']) {
|
|
102
|
+
if (Array.isArray(value?.[key]))
|
|
103
|
+
return value[key];
|
|
104
|
+
}
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
function normalizePlugin(summary, detail) {
|
|
108
|
+
const raw = { summary, detail };
|
|
109
|
+
const id = String(detail?.id || summary?.id || summary?.plugin_id || summary?.name || 'unknown');
|
|
110
|
+
const name = String(detail?.name || summary?.name || id);
|
|
111
|
+
const sourceText = String(detail?.source || detail?.marketplaceSource || summary?.source || summary?.marketplaceSource || '').toLowerCase();
|
|
112
|
+
const source = sourceText.includes('marketplace') ? 'marketplace'
|
|
113
|
+
: sourceText.includes('remote') ? 'remote'
|
|
114
|
+
: sourceText.includes('local') ? 'local'
|
|
115
|
+
: 'unknown';
|
|
116
|
+
return {
|
|
117
|
+
id,
|
|
118
|
+
name,
|
|
119
|
+
source,
|
|
120
|
+
installed: boolish(detail?.installed ?? summary?.installed, true),
|
|
121
|
+
enabled: boolish(detail?.enabled ?? summary?.enabled, true),
|
|
122
|
+
default_prompts: normalizeList(detail?.default_prompts || detail?.defaultPrompts || detail?.prompts),
|
|
123
|
+
remote_mcp_servers: normalizeMcpServers(detail?.remote_mcp_servers || detail?.remoteMcpServers || detail?.mcp_servers || detail?.mcpServers),
|
|
124
|
+
unavailable_app_templates: normalizeList(detail?.unavailable_app_templates || detail?.unavailableAppTemplates || detail?.app_templates_unavailable),
|
|
125
|
+
raw
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function normalizeMcpServers(value) {
|
|
129
|
+
const rows = Array.isArray(value) ? value : value && typeof value === 'object' ? Object.entries(value).map(([name, row]) => ({ name, ...(row || {}) })) : [];
|
|
130
|
+
return rows.map((row, index) => ({
|
|
131
|
+
name: String(row?.name || row?.id || `remote-mcp-${index + 1}`),
|
|
132
|
+
url: stringOrNull(row?.url || row?.endpoint),
|
|
133
|
+
auth_type: stringOrNull(row?.auth_type || row?.authType || row?.auth)
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
function normalizeList(value) {
|
|
137
|
+
return Array.isArray(value) ? value.filter(Boolean).map(String) : value ? [String(value)] : [];
|
|
138
|
+
}
|
|
139
|
+
function stringOrNull(value) {
|
|
140
|
+
const text = String(value || '').trim();
|
|
141
|
+
return text ? text : null;
|
|
142
|
+
}
|
|
143
|
+
function boolish(value, fallback = false) {
|
|
144
|
+
if (value === true || value === 'true')
|
|
145
|
+
return true;
|
|
146
|
+
if (value === false || value === 'false')
|
|
147
|
+
return false;
|
|
148
|
+
return fallback;
|
|
149
|
+
}
|
|
150
|
+
function fakePluginList() {
|
|
151
|
+
const count = Math.max(1, Number(process.env.SKS_CODEX_PLUGIN_JSON_FAKE_COUNT || 1) || 1);
|
|
152
|
+
return {
|
|
153
|
+
marketplace_available: true,
|
|
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
|
+
}))
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function fakePluginDetail(pluginId) {
|
|
165
|
+
return {
|
|
166
|
+
id: pluginId,
|
|
167
|
+
name: pluginId,
|
|
168
|
+
source: 'marketplace',
|
|
169
|
+
installed: true,
|
|
170
|
+
enabled: true,
|
|
171
|
+
default_prompts: ['Use the fixture plugin safely.'],
|
|
172
|
+
remote_mcp_servers: [{ name: 'fixture-db-docs', url: 'https://mcp.example.test', auth_type: 'oauth' }],
|
|
173
|
+
unavailable_app_templates: ['fixture-desktop-template']
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=codex-plugin-json.js.map
|
|
@@ -20,6 +20,7 @@ import { runCodexLaunchPreflight } from '../preflight/parallel-preflight-engine.
|
|
|
20
20
|
import { diffCodexAppUiSnapshots, writeCodexAppUiSnapshot } from '../codex-app/codex-app-ui-state-snapshot.js';
|
|
21
21
|
import { checkSksUpdateNotice } from '../update/update-notice.js';
|
|
22
22
|
import { createMadDbCapability, MAD_DB_ACK } from '../mad-db/mad-db-capability.js';
|
|
23
|
+
import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
|
|
23
24
|
export async function madHighCommand(args = [], deps = {}) {
|
|
24
25
|
const subcommand = firstSubcommand(args);
|
|
25
26
|
if (subcommand)
|
|
@@ -36,6 +37,10 @@ export async function madHighCommand(args = [], deps = {}) {
|
|
|
36
37
|
process.exitCode = 1;
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
40
|
+
// Zellij is checked the same way Codex is, but it stays NON-blocking: a
|
|
41
|
+
// failed or skipped zellij upgrade never prevents the MAD launch.
|
|
42
|
+
const zellijUpdate = deps.maybePromptZellijUpdateForLaunch ? await deps.maybePromptZellijUpdateForLaunch(args, { label: 'MAD launch' }).catch(() => ({ status: 'error' })) : { status: 'skipped' };
|
|
43
|
+
void zellijUpdate;
|
|
39
44
|
const depStatus = deps.ensureMadLaunchDependencies ? await deps.ensureMadLaunchDependencies(args) : { ready: true, actions: [] };
|
|
40
45
|
if (!depStatus.ready) {
|
|
41
46
|
console.error('SKS MAD launch blocked by missing dependencies.');
|
|
@@ -379,6 +384,7 @@ async function activateMadZellijPermissionState(cwd = process.cwd(), args = [])
|
|
|
379
384
|
const has = (scope) => allowedScopes.has(scope);
|
|
380
385
|
const dbWriteAllowed = has('db_write');
|
|
381
386
|
const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: 'sks --mad Zellij scoped high-power maintenance session' });
|
|
387
|
+
await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch(() => null);
|
|
382
388
|
const protectedCore = resolveProtectedCore({ packageRoot: packageRoot(), targetRoot: cwd });
|
|
383
389
|
// The interactive launch 'before' snapshot is only persisted (env + policy json)
|
|
384
390
|
// and is never compared against an 'after' snapshot during the session, so the
|
|
@@ -564,6 +570,7 @@ function codexLbImmediateLaunchOpts(args = [], lb = {}, opts = {}) {
|
|
|
564
570
|
}
|
|
565
571
|
export async function madSksFixture(root) {
|
|
566
572
|
const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: '$MAD-SKS fixture permission gate' });
|
|
573
|
+
await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch(() => null);
|
|
567
574
|
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 };
|
|
568
575
|
await writeJsonAtomic(path.join(dir, 'mad-sks-gate.json'), gate);
|
|
569
576
|
return { mission_id: id, dir, gate };
|
|
@@ -738,6 +745,7 @@ async function materializeMadSksRun(root, targetRoot, permission, userIntent, js
|
|
|
738
745
|
if (!(await exists(path.join(root, '.sneakoscope'))))
|
|
739
746
|
await initProject(root, {});
|
|
740
747
|
const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: userIntent });
|
|
748
|
+
await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch(() => null);
|
|
741
749
|
const before = await snapshotProtectedCore(packageRoot(), 'before');
|
|
742
750
|
const authorization = opts.authorizationManifest || createMadSksAuthorizationManifest({ permission, userIntent });
|
|
743
751
|
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,11 +16,13 @@ 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';
|
|
25
|
+
import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
|
|
23
26
|
const NARUTO_RESULT_SCHEMA = 'sks.naruto-command-result.v1';
|
|
24
27
|
const NARUTO_ROUTE = '$Naruto';
|
|
25
28
|
// $Naruto — Shadow Clone Swarm (影分身 / Kage Bunshin no Jutsu).
|
|
@@ -40,6 +43,12 @@ export async function narutoCommand(commandOrArgs = 'naruto', maybeArgs = []) {
|
|
|
40
43
|
return narutoWorkers(parsed);
|
|
41
44
|
if (parsed.action === 'proof')
|
|
42
45
|
return narutoProof(parsed);
|
|
46
|
+
// Like the Codex CLI update prompt: check the installed zellij version and
|
|
47
|
+
// offer an upgrade to the latest stable release before the live session
|
|
48
|
+
// opens. Never blocks the run.
|
|
49
|
+
if (!parsed.json && !parsed.mock && !parsed.noOpenZellij) {
|
|
50
|
+
await maybePromptZellijUpdateForLaunch(args, { label: '$Naruto launch' }).catch(() => undefined);
|
|
51
|
+
}
|
|
43
52
|
return narutoRun(parsed);
|
|
44
53
|
}
|
|
45
54
|
async function narutoRun(parsed) {
|
|
@@ -73,6 +82,7 @@ async function narutoRun(parsed) {
|
|
|
73
82
|
maxAgentCount: MAX_NARUTO_AGENT_COUNT
|
|
74
83
|
});
|
|
75
84
|
const mission = await createMission(root, { mode: 'naruto', prompt: parsed.prompt });
|
|
85
|
+
await writeCodex0138CapabilityArtifacts(root, { missionId: mission.id }).catch(() => null);
|
|
76
86
|
const gitWorktreeCapability = writeCapable
|
|
77
87
|
? await evaluateGitWorktreeCapability({ root, missionId: mission.id })
|
|
78
88
|
: null;
|
|
@@ -297,6 +307,10 @@ async function narutoRun(parsed) {
|
|
|
297
307
|
console.log(' parallelism mode: ' + parsed.parallelism);
|
|
298
308
|
if (activeSlots < roster.agent_count)
|
|
299
309
|
console.log(' cap reasons: ' + (governor.reasons.join(', ') || 'host safety cap'));
|
|
310
|
+
// Backpressure used to throttle silently (50% when throttled, 25% when
|
|
311
|
+
// saturated); always tell the operator when host pressure reduced workers.
|
|
312
|
+
if (governor.backpressure !== 'normal')
|
|
313
|
+
console.log(' backpressure: ' + governor.backpressure + ' — host resource pressure reduced active workers (memory/cpu/fd/disk thresholds)');
|
|
300
314
|
if (parsed.parallelism !== 'safe' && activeSlots < 10)
|
|
301
315
|
console.log(' warning: active workers below 10 in non-safe mode');
|
|
302
316
|
}
|
|
@@ -382,7 +396,7 @@ async function narutoRun(parsed) {
|
|
|
382
396
|
&& Number(parallelRuntime.max_observed_active_workers || 0) >= Math.min(16, activeSlots));
|
|
383
397
|
await writeJsonAtomic(path.join(mission.dir, 'naruto-gate.json'), {
|
|
384
398
|
schema: 'sks.naruto-gate.v1',
|
|
385
|
-
passed: result.ok === true && nativeProofOk && finalAccepted,
|
|
399
|
+
passed: result.ok === true && nativeProofOk && finalAccepted && parallelRuntimeOk,
|
|
386
400
|
mission_id: mission.id,
|
|
387
401
|
clone_roster_built: true,
|
|
388
402
|
clone_count: roster.agent_count,
|
|
@@ -416,6 +430,18 @@ async function narutoRun(parsed) {
|
|
|
416
430
|
});
|
|
417
431
|
const clones = result.roster?.agent_count ?? roster.agent_count;
|
|
418
432
|
const localWorkerSummary = summarizeNarutoLocalWorkerResult(localWorker, result);
|
|
433
|
+
// Finalizer policy: when local LLM workers contributed patches, the GPT
|
|
434
|
+
// final arbiter must have accepted before patches are considered final.
|
|
435
|
+
const finalizer = evaluateNarutoFinalizer({
|
|
436
|
+
localParticipated: Number(localWorkerSummary?.selected_worker_count || 0) > 0,
|
|
437
|
+
gptFinalStatus: result.proof?.gpt_final_status || null,
|
|
438
|
+
applyPatches: writeCapable
|
|
439
|
+
});
|
|
440
|
+
await writeJsonAtomic(path.join(mission.dir, 'naruto-finalizer.json'), {
|
|
441
|
+
...finalizer,
|
|
442
|
+
generated_at: nowIso(),
|
|
443
|
+
mission_id: mission.id
|
|
444
|
+
});
|
|
419
445
|
const summary = {
|
|
420
446
|
schema: NARUTO_RESULT_SCHEMA,
|
|
421
447
|
ok: result.ok === true,
|
|
@@ -475,6 +501,7 @@ async function narutoRun(parsed) {
|
|
|
475
501
|
passed: parallelRuntime.passed
|
|
476
502
|
} : null,
|
|
477
503
|
local_worker: localWorkerSummary,
|
|
504
|
+
finalizer,
|
|
478
505
|
proof: result.proof?.status || 'missing',
|
|
479
506
|
run: compactNarutoRunResult(result),
|
|
480
507
|
zellij: null
|
|
@@ -487,6 +514,8 @@ async function narutoRun(parsed) {
|
|
|
487
514
|
console.log('Backend: ' + result.backend);
|
|
488
515
|
console.log('Roles: ' + roleDistribution.entries.map((entry) => `${entry.role}:${entry.count}`).join(', '));
|
|
489
516
|
console.log('Proof: ' + summary.proof);
|
|
517
|
+
if (!finalizer.ok)
|
|
518
|
+
console.log('Finalizer: blocked — ' + finalizer.blockers.join(', '));
|
|
490
519
|
if (summary.parallel_runtime) {
|
|
491
520
|
console.log('$Naruto parallel proof:');
|
|
492
521
|
console.log(' max active workers: ' + summary.parallel_runtime.max_observed_active_workers);
|
|
@@ -13,9 +13,17 @@ import { scanDbSafety } from '../db-safety.js';
|
|
|
13
13
|
import { maybeFinalizeRoute } from '../proof/auto-finalize.js';
|
|
14
14
|
import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
|
|
15
15
|
import { flag, promptOf, readBoundedIntegerFlag, readFlagValue, readMaxCycles, resolveMissionId, safeReadTextFile } from './command-utils.js';
|
|
16
|
+
import { runCodexAppHandoff, qaLoopShouldRequestAppHandoff } from '../codex-app/codex-app-handoff.js';
|
|
17
|
+
import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
|
|
18
|
+
import { writeCodexAccountUsageArtifacts } from '../usage/codex-account-usage.js';
|
|
19
|
+
import { buildQaLoopBudgetPolicy, selectQaLoopEscalatedEffort } from '../qa-loop/qa-loop-budget-policy.js';
|
|
20
|
+
import { writeCodexModelEffortCapabilityArtifact } from '../codex-control/codex-model-capabilities.js';
|
|
21
|
+
import { discoverImageArtifactsInDir, writeImageArtifactPathContract } from '../image/image-artifact-path-contract.js';
|
|
22
|
+
import { pluginAppTemplatePolicy } from '../codex-plugins/codex-plugin-json.js';
|
|
23
|
+
import { confirmQaLoopAppHandoff } from '../qa-loop/qa-loop-app-handoff-confirmation.js';
|
|
16
24
|
import fsp from 'node:fs/promises';
|
|
17
25
|
export async function qaLoopCommand(sub, args = []) {
|
|
18
|
-
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']);
|
|
19
27
|
const action = known.has(sub) ? sub : 'prepare';
|
|
20
28
|
const actionArgs = action === 'prepare' && sub && !known.has(sub) ? [sub, ...args] : args;
|
|
21
29
|
if (action === 'prepare')
|
|
@@ -26,13 +34,16 @@ export async function qaLoopCommand(sub, args = []) {
|
|
|
26
34
|
return qaLoopRun(actionArgs);
|
|
27
35
|
if (action === 'status')
|
|
28
36
|
return qaLoopStatus(actionArgs);
|
|
37
|
+
if (action === 'app-confirm')
|
|
38
|
+
return qaLoopAppConfirm(actionArgs);
|
|
29
39
|
console.log(`SKS QA-LOOP
|
|
30
40
|
|
|
31
41
|
Usage:
|
|
32
42
|
sks qa-loop prepare "target"
|
|
33
43
|
sks qa-loop answer <mission-id|latest> <answers.json>
|
|
34
|
-
sks qa-loop run <mission-id|latest> [--mock] [--max-cycles N]
|
|
35
|
-
sks qa-loop
|
|
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 "..."
|
|
46
|
+
sks qa-loop status <mission-id|latest> [--desktop]
|
|
36
47
|
`);
|
|
37
48
|
}
|
|
38
49
|
function qaRoute() {
|
|
@@ -136,6 +147,101 @@ async function qaLoopRun(args) {
|
|
|
136
147
|
const qaGate = await readJson(path.join(dir, 'qa-gate.json'), {});
|
|
137
148
|
const reportFile = qaGate.qa_report_file;
|
|
138
149
|
const uiRequired = qaUiRequired(contract.answers || {});
|
|
150
|
+
const capabilityArtifact = await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), report: null }));
|
|
151
|
+
const usageArtifact = await writeCodexAccountUsageArtifacts(root, { missionId: id }).catch((err) => ({ error: err?.message || String(err), snapshot: null }));
|
|
152
|
+
const budgetPolicy = buildQaLoopBudgetPolicy({ usage: usageArtifact?.snapshot || null, provider: 'codex-sdk' });
|
|
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 }));
|
|
155
|
+
const effortEscalation = selectQaLoopEscalatedEffort({
|
|
156
|
+
failureCount: Number(qaGate.safe_fix_attempts || qaGate.failure_count || 0),
|
|
157
|
+
currentEffort: String(profile || 'high').replace(/^sks-(?:logic|agent)-/, '').replace(/-fast$/, '') || 'high',
|
|
158
|
+
capability: effortCapabilityArtifact?.capability || undefined
|
|
159
|
+
});
|
|
160
|
+
await writeJsonAtomic(path.join(dir, 'qa-loop', 'qa-loop-effort-escalation.json'), effortEscalation);
|
|
161
|
+
const discoveredImages = await discoverImageArtifactsInDir(dir).catch(() => []);
|
|
162
|
+
const imagePathContract = discoveredImages.length
|
|
163
|
+
? await writeQaLoopImagePathContract(root, dir, id, discoveredImages)
|
|
164
|
+
: null;
|
|
165
|
+
const pluginInventory = await readJson(path.join(root, '.sneakoscope', 'codex-plugin-inventory.json'), null);
|
|
166
|
+
const pluginPolicy = pluginInventory?.schema === 'sks.codex-plugin-inventory.v1' ? pluginAppTemplatePolicy(pluginInventory) : null;
|
|
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';
|
|
171
|
+
const appHandoffRequested = qaLoopShouldRequestAppHandoff({
|
|
172
|
+
args,
|
|
173
|
+
uiRequired,
|
|
174
|
+
visualArtifactsPresent: discoveredImages.length > 0,
|
|
175
|
+
pluginAppTemplateUnavailable: Boolean(pluginPolicy?.unavailable_app_templates?.length),
|
|
176
|
+
userRequestedDesktopReview: appHandoffRequired
|
|
177
|
+
});
|
|
178
|
+
const appHandoff = appHandoffRequested || appHandoffRequired
|
|
179
|
+
? await runCodexAppHandoff(root, {
|
|
180
|
+
schema: 'sks.codex-app-handoff-request.v1',
|
|
181
|
+
mission_id: id,
|
|
182
|
+
route: '$QA-LOOP',
|
|
183
|
+
reason: appHandoffRequired ? 'desktop_app_review_required' : 'desktop_app_review_requested',
|
|
184
|
+
thread_ref: null,
|
|
185
|
+
workspace_path: root,
|
|
186
|
+
artifacts: [
|
|
187
|
+
'decision-contract.json',
|
|
188
|
+
'qa-gate.json',
|
|
189
|
+
'qa-ledger.json',
|
|
190
|
+
reportFile,
|
|
191
|
+
capabilityArtifact && !capabilityArtifact.error ? 'codex-0138-capability.json' : '',
|
|
192
|
+
imagePathContract ? 'qa-loop/image-artifact-path-contract.json' : ''
|
|
193
|
+
].filter(Boolean),
|
|
194
|
+
prompt: mission.prompt || 'QA-LOOP desktop handoff',
|
|
195
|
+
require_desktop: appHandoffRequired,
|
|
196
|
+
capability_required: 'codex-0.138',
|
|
197
|
+
launch_mode: flag(args, '--app-handoff-artifact-only') ? 'artifact-only' : launchMode
|
|
198
|
+
}).catch((err) => ({
|
|
199
|
+
ok: false,
|
|
200
|
+
status: 'blocked_for_desktop_review',
|
|
201
|
+
artifact_path: path.join(dir, 'qa-loop', 'app-handoff.json'),
|
|
202
|
+
blockers: [`codex_app_handoff_failed:${err?.message || String(err)}`],
|
|
203
|
+
desktop_handoff_supported: false,
|
|
204
|
+
launch_attempt: null
|
|
205
|
+
}))
|
|
206
|
+
: null;
|
|
207
|
+
if (appHandoff || imagePathContract) {
|
|
208
|
+
const latestGate = await readJson(path.join(dir, 'qa-gate.json'), qaGate);
|
|
209
|
+
const nextGate = {
|
|
210
|
+
...latestGate,
|
|
211
|
+
desktop_app_handoff_required: appHandoffRequired,
|
|
212
|
+
desktop_app_handoff_status: appHandoff ? appHandoff.status : 'not_requested',
|
|
213
|
+
desktop_app_handoff_artifact: appHandoff ? path.relative(dir, appHandoff.artifact_path) : null,
|
|
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,
|
|
218
|
+
desktop_app_handoff_is_web_ui_evidence: false,
|
|
219
|
+
image_artifact_path_contract_present: Boolean(imagePathContract),
|
|
220
|
+
image_artifact_path_contract_artifact: imagePathContract ? 'qa-loop/image-artifact-path-contract.json' : null,
|
|
221
|
+
image_artifact_path_contract_blockers: imagePathContract?.contract?.blockers || [],
|
|
222
|
+
blockers: Array.from(new Set([
|
|
223
|
+
...(latestGate.blockers || []),
|
|
224
|
+
...(appHandoffRequired && appHandoff && appHandoff.ok !== true ? ['blocked_for_desktop_review'] : []),
|
|
225
|
+
...(appHandoffRequired && latestGate.desktop_app_handoff_confirmed !== true ? ['desktop_app_handoff_confirmation_missing'] : []),
|
|
226
|
+
...(imagePathContract?.contract?.blockers || [])
|
|
227
|
+
])),
|
|
228
|
+
notes: [
|
|
229
|
+
...(latestGate.notes || []),
|
|
230
|
+
...(appHandoff ? ['Codex Desktop /app handoff is tracked separately and is not web UI verification evidence.'] : []),
|
|
231
|
+
...(imagePathContract ? ['Image artifacts expose real saved file paths for follow-up visual edits.'] : [])
|
|
232
|
+
]
|
|
233
|
+
};
|
|
234
|
+
await writeJsonAtomic(path.join(dir, 'qa-gate.json'), nextGate);
|
|
235
|
+
if (appHandoffRequired && appHandoff && appHandoff.ok !== true) {
|
|
236
|
+
await maybeFinalizeRoute(root, { missionId: id, route: '$QA-LOOP', gateFile: 'qa-gate.json', gate: nextGate, artifacts: ['qa-gate.json', 'qa-ledger.json', reportFile, 'qa-loop/app-handoff.json', 'completion-proof.json'], statusHint: 'blocked', blockers: nextGate.blockers, command: { cmd: `sks qa-loop run ${id} --app-handoff-required`, status: 2 } });
|
|
237
|
+
await setCurrent(root, { mission_id: id, mode: 'QALOOP', phase: 'QALOOP_BLOCKED_DESKTOP_APP_HANDOFF', questions_allowed: true });
|
|
238
|
+
if (flag(args, '--json'))
|
|
239
|
+
return console.log(JSON.stringify({ schema: 'sks.qa-loop-run.v1', ok: false, status: 'blocked_for_desktop_review', mission_id: id, app_handoff: appHandoff, gate: nextGate }, null, 2));
|
|
240
|
+
console.error('QA-LOOP blocked: Codex Desktop /app handoff is required but unavailable or still pending.');
|
|
241
|
+
process.exitCode = 2;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
139
245
|
if (uiRequired && !mock) {
|
|
140
246
|
const chrome = await codexChromeExtensionStatus();
|
|
141
247
|
if (!chrome.ok) {
|
|
@@ -237,7 +343,7 @@ async function qaLoopRun(args) {
|
|
|
237
343
|
for (let cycle = 1; cycle <= maxCycles; cycle += 1) {
|
|
238
344
|
const cycleDir = path.join(dir, 'qa-loop', `cycle-${cycle}`);
|
|
239
345
|
const outputFile = path.join(cycleDir, 'final.md');
|
|
240
|
-
const prompt = buildQaLoopPrompt({ id, mission, contract, cycle, previous: last, reportFile });
|
|
346
|
+
const prompt = buildQaLoopPrompt({ id, mission, contract, cycle, previous: last, reportFile, imagePathContract: imagePathContract?.contract || null, appHandoff });
|
|
241
347
|
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'qaloop.cycle.start', cycle });
|
|
242
348
|
const result = await runCodexExec({ root, prompt, outputFile, json: true, profile, logDir: cycleDir });
|
|
243
349
|
await writeJsonAtomic(path.join(cycleDir, 'process.json'), { code: result.code, stdout_tail: result.stdout, stderr_tail: result.stderr, stdout_bytes: result.stdoutBytes, stderr_bytes: result.stderrBytes, truncated: result.truncated, timed_out: result.timedOut });
|
|
@@ -276,8 +382,11 @@ async function qaLoopStatus(args) {
|
|
|
276
382
|
const status = await qaStatus(dir);
|
|
277
383
|
const nativeAgentPlan = await readJson(path.join(dir, 'qa-agent-plan.json'), null);
|
|
278
384
|
const agentSessions = await readJson(path.join(dir, 'agents', 'agent-sessions.json'), null);
|
|
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';
|
|
279
388
|
if (flag(args, '--json'))
|
|
280
|
-
return console.log(JSON.stringify({ mission, state, qa: status, 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));
|
|
281
390
|
console.log('SKS QA-LOOP Status\n');
|
|
282
391
|
console.log(`Mission: ${id}`);
|
|
283
392
|
console.log(`Phase: ${state.phase || mission.phase}`);
|
|
@@ -286,5 +395,38 @@ async function qaLoopStatus(args) {
|
|
|
286
395
|
console.log(`Gate: ${status.gate?.passed ? 'passed' : 'not passed'}`);
|
|
287
396
|
if (status.gate?.reasons?.length)
|
|
288
397
|
console.log(`Reasons: ${status.gate.reasons.join(', ')}`);
|
|
398
|
+
if (flag(args, '--desktop')) {
|
|
399
|
+
console.log('Desktop:');
|
|
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'}`);
|
|
404
|
+
if (desktop?.operator_instruction?.prompt_artifact)
|
|
405
|
+
console.log(` prompt: ${desktop.operator_instruction.prompt_artifact}`);
|
|
406
|
+
console.log(' web evidence: not a substitute for Codex Chrome Extension web UI verification');
|
|
407
|
+
}
|
|
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
|
+
}
|
|
423
|
+
async function writeQaLoopImagePathContract(root, dir, missionId, images) {
|
|
424
|
+
const primary = await writeImageArtifactPathContract(root, {
|
|
425
|
+
missionId,
|
|
426
|
+
images,
|
|
427
|
+
artifactPath: path.join(dir, 'image-artifact-path-contract.json')
|
|
428
|
+
});
|
|
429
|
+
await writeJsonAtomic(path.join(dir, 'qa-loop', 'image-artifact-path-contract.json'), primary.contract);
|
|
430
|
+
return primary;
|
|
289
431
|
}
|
|
290
432
|
//# sourceMappingURL=qa-loop-command.js.map
|