sneakoscope 2.0.17 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +135 -90
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/commands/doctor.js +39 -1
  8. package/dist/commands/mad-sks.js +2 -0
  9. package/dist/commands/zellij.js +58 -1
  10. package/dist/core/agents/agent-effort-policy.js +7 -1
  11. package/dist/core/agents/agent-scheduler.js +32 -24
  12. package/dist/core/agents/native-cli-session-swarm.js +22 -2
  13. package/dist/core/codex-app/codex-app-handoff.js +98 -0
  14. package/dist/core/codex-app/codex-app-launcher.js +103 -0
  15. package/dist/core/codex-control/codex-0138-capability.js +102 -0
  16. package/dist/core/codex-control/codex-model-capabilities.js +62 -0
  17. package/dist/core/codex-control/codex-model-metadata.js +91 -0
  18. package/dist/core/codex-control/codex-sdk-config-policy.js +1 -1
  19. package/dist/core/codex-control/codex-task-runner.js +1 -1
  20. package/dist/core/codex-plugins/codex-plugin-cache.js +38 -0
  21. package/dist/core/codex-plugins/codex-plugin-diff.js +73 -0
  22. package/dist/core/codex-plugins/codex-plugin-json.js +176 -0
  23. package/dist/core/commands/mad-sks-command.js +8 -0
  24. package/dist/core/commands/naruto-command.js +30 -1
  25. package/dist/core/commands/qa-loop-command.js +147 -5
  26. package/dist/core/doctor/codex-0138-doctor.js +104 -0
  27. package/dist/core/doctor/doctor-readiness-matrix.js +11 -0
  28. package/dist/core/effort-orchestrator.js +9 -0
  29. package/dist/core/fsx.js +1 -1
  30. package/dist/core/hooks-runtime.js +6 -9
  31. package/dist/core/image/image-artifact-path-contract.js +101 -0
  32. package/dist/core/image/image-artifact-registry.js +33 -0
  33. package/dist/core/image-ux-review/imagegen-adapter.js +49 -17
  34. package/dist/core/mad-db/mad-db-result-lifecycle.js +71 -0
  35. package/dist/core/mcp/mcp-plugin-inventory.js +29 -0
  36. package/dist/core/mcp/mcp-server-policy.js +24 -0
  37. package/dist/core/qa-loop/qa-loop-app-handoff-confirmation.js +51 -0
  38. package/dist/core/qa-loop/qa-loop-budget-policy.js +37 -0
  39. package/dist/core/qa-loop.js +70 -3
  40. package/dist/core/release/release-gate-cache-v2.js +47 -5
  41. package/dist/core/usage/codex-account-usage.js +139 -0
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/zellij/zellij-slot-column-anchor.js +16 -7
  44. package/dist/core/zellij/zellij-slot-pane-renderer.js +23 -2
  45. package/dist/core/zellij/zellij-slot-telemetry.js +65 -12
  46. package/dist/core/zellij/zellij-ui-mode.js +8 -1
  47. package/dist/core/zellij/zellij-update.js +307 -0
  48. package/dist/core/zellij/zellij-worker-pane-manager.js +211 -145
  49. package/dist/scripts/release-gate-existence-audit.js +5 -1
  50. package/package.json +46 -3
  51. package/schemas/codex-app/codex-app-handoff.schema.json +20 -0
  52. package/schemas/codex-plugins/codex-plugin-inventory.schema.json +32 -0
  53. package/schemas/image/image-artifact-path-contract.schema.json +32 -0
  54. package/schemas/usage/codex-account-usage.schema.json +27 -0
  55. package/dist/core/naruto/naruto-work-stealing.js +0 -11
  56. package/dist/core/zellij/zellij-right-column-layout-proof.js +0 -42
@@ -0,0 +1,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 status <mission-id|latest>
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