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.
Files changed (41) hide show
  1. package/README.md +127 -71
  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/mad-sks.js +2 -0
  8. package/dist/commands/zellij.js +58 -1
  9. package/dist/core/agents/agent-scheduler.js +32 -24
  10. package/dist/core/agents/native-cli-session-swarm.js +22 -2
  11. package/dist/core/codex-app/codex-app-handoff.js +30 -9
  12. package/dist/core/codex-app/codex-app-launcher.js +103 -0
  13. package/dist/core/codex-control/codex-0138-capability.js +42 -4
  14. package/dist/core/codex-control/codex-0139-capability.js +102 -0
  15. package/dist/core/codex-control/codex-model-capabilities.js +25 -4
  16. package/dist/core/codex-control/codex-model-metadata.js +91 -0
  17. package/dist/core/codex-plugins/codex-plugin-cache.js +38 -0
  18. package/dist/core/codex-plugins/codex-plugin-diff.js +73 -0
  19. package/dist/core/codex-plugins/codex-plugin-json.js +35 -11
  20. package/dist/core/commands/mad-sks-command.js +8 -0
  21. package/dist/core/commands/naruto-command.js +29 -0
  22. package/dist/core/commands/qa-loop-command.js +41 -6
  23. package/dist/core/fsx.js +1 -1
  24. package/dist/core/image/image-artifact-path-contract.js +2 -0
  25. package/dist/core/image/image-artifact-registry.js +33 -0
  26. package/dist/core/image-ux-review/imagegen-adapter.js +27 -16
  27. package/dist/core/pipeline-internals/runtime-core.js +4 -2
  28. package/dist/core/qa-loop/qa-loop-app-handoff-confirmation.js +51 -0
  29. package/dist/core/qa-loop/qa-loop-budget-policy.js +1 -1
  30. package/dist/core/qa-loop.js +44 -3
  31. package/dist/core/release/release-gate-cache-v2.js +47 -5
  32. package/dist/core/usage/codex-account-usage.js +77 -16
  33. package/dist/core/version.js +1 -1
  34. package/dist/core/zellij/zellij-slot-pane-renderer.js +5 -2
  35. package/dist/core/zellij/zellij-slot-telemetry.js +65 -12
  36. package/dist/core/zellij/zellij-ui-mode.js +8 -1
  37. package/dist/core/zellij/zellij-update.js +307 -0
  38. package/dist/core/zellij/zellij-worker-pane-manager.js +211 -145
  39. package/package.json +23 -2
  40. package/dist/core/naruto/naruto-work-stealing.js +0 -11
  41. package/dist/core/zellij/zellij-right-column-layout-proof.js +0 -42
@@ -0,0 +1,307 @@
1
+ import https from 'node:https';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import readline from 'node:readline';
5
+ import { ensureDir, globalSksRoot, nowIso, readJson, runProcess, writeJsonAtomic } from '../fsx.js';
6
+ import { guardContextForRoute, guardedPackageInstall } from '../safety/mutation-guard.js';
7
+ import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
8
+ import { checkZellijCapability } from './zellij-capability.js';
9
+ import { compareVersionLike, parseZellijVersionText } from './zellij-command.js';
10
+ export const ZELLIJ_UPDATE_NOTICE_SCHEMA = 'sks.zellij-update-notice.v1';
11
+ const ZELLIJ_RELEASES_API_PATH = '/repos/zellij-org/zellij/releases/latest';
12
+ export function zellijUpgradeCommandHint(missing = false) {
13
+ if (process.platform === 'darwin')
14
+ return missing ? 'brew install zellij' : 'brew upgrade zellij';
15
+ if (process.platform === 'linux')
16
+ return 'cargo install --locked zellij # or your distro package manager';
17
+ return 'See https://zellij.dev/documentation/installation';
18
+ }
19
+ /**
20
+ * Resolve the latest STABLE zellij version. GitHub's /releases/latest endpoint
21
+ * already excludes prereleases and drafts. Results are cached on disk
22
+ * (default TTL 6h, override with SKS_ZELLIJ_UPDATE_TTL_MS) so command launches
23
+ * stay fast and offline-safe. SKS_ZELLIJ_LATEST_VERSION pins the value for
24
+ * tests and air-gapped environments.
25
+ */
26
+ export async function fetchLatestZellijVersion(input = {}) {
27
+ const env = input.env || process.env;
28
+ const pinned = stripVersionPrefix(String(env.SKS_ZELLIJ_LATEST_VERSION || '').trim());
29
+ if (pinned)
30
+ return { version: parseZellijVersionText(pinned) || pinned, source: 'env' };
31
+ const ttlMs = normalizePositiveInt(env.SKS_ZELLIJ_UPDATE_TTL_MS, 6 * 60 * 60 * 1000);
32
+ const cachePath = zellijUpdateCachePath();
33
+ const cached = await readJson(cachePath, null);
34
+ if (cached?.schema === ZELLIJ_UPDATE_NOTICE_SCHEMA && cached.latest_version && Date.now() - Date.parse(cached.checked_at || '') < ttlMs) {
35
+ return { version: cached.latest_version, source: 'cache' };
36
+ }
37
+ try {
38
+ const tag = await githubLatestTag(input.timeoutMs || normalizePositiveInt(env.SKS_ZELLIJ_UPDATE_TIMEOUT_MS, 2500));
39
+ // GitHub release tags carry a leading "v" (v0.44.3); \b-based version
40
+ // parsing cannot start inside "v0", so strip the prefix first.
41
+ const version = parseZellijVersionText(stripVersionPrefix(tag));
42
+ if (!version)
43
+ throw new Error(`zellij_latest_tag_unparsed:${tag}`);
44
+ return { version, source: 'github-releases' };
45
+ }
46
+ catch (err) {
47
+ return {
48
+ version: cached?.latest_version || null,
49
+ source: 'error',
50
+ error: err?.message || String(err)
51
+ };
52
+ }
53
+ }
54
+ export async function checkZellijUpdateNotice(input = {}) {
55
+ const env = input.env || process.env;
56
+ const ttlMs = normalizePositiveInt(env.SKS_ZELLIJ_UPDATE_TTL_MS, 6 * 60 * 60 * 1000);
57
+ if (env.SKS_SKIP_ZELLIJ_UPDATE === '1' || env.SKS_DISABLE_UPDATE_NOTICE === '1') {
58
+ return persistNotice(input.missionDir, {
59
+ schema: ZELLIJ_UPDATE_NOTICE_SCHEMA,
60
+ checked_at: nowIso(),
61
+ current_version: null,
62
+ latest_version: null,
63
+ update_available: false,
64
+ zellij_missing: false,
65
+ source: 'disabled',
66
+ cache_ttl_ms: ttlMs,
67
+ upgrade_command: zellijUpgradeCommandHint(),
68
+ message: 'Zellij update notice disabled by environment.'
69
+ });
70
+ }
71
+ const capability = await checkZellijCapability({ require: false, writeReport: false }).catch(() => null);
72
+ const current = capability?.version || null;
73
+ const missing = capability?.status === 'missing' || !capability;
74
+ const fetchInput = { env };
75
+ if (input.timeoutMs !== undefined)
76
+ fetchInput.timeoutMs = input.timeoutMs;
77
+ const latest = await fetchLatestZellijVersion(fetchInput);
78
+ const updateAvailable = Boolean(!missing && current && latest.version && compareVersionLike(latest.version, current) > 0);
79
+ const notice = {
80
+ schema: ZELLIJ_UPDATE_NOTICE_SCHEMA,
81
+ checked_at: nowIso(),
82
+ current_version: current,
83
+ latest_version: latest.version,
84
+ update_available: updateAvailable,
85
+ zellij_missing: missing,
86
+ source: latest.source,
87
+ cache_ttl_ms: ttlMs,
88
+ upgrade_command: zellijUpgradeCommandHint(missing),
89
+ message: missing
90
+ ? `Zellij is not installed. Install with: ${zellijUpgradeCommandHint(true)}`
91
+ : updateAvailable
92
+ ? `Zellij ${latest.version} is available; current ${current}. Upgrade with: ${zellijUpgradeCommandHint()}`
93
+ : `Zellij ${current || 'unknown'} is current enough.`,
94
+ ...(latest.error ? { error: latest.error } : {})
95
+ };
96
+ if (latest.source === 'github-releases') {
97
+ await ensureDir(path.dirname(zellijUpdateCachePath())).catch(() => undefined);
98
+ await writeJsonAtomic(zellijUpdateCachePath(), notice).catch(() => undefined);
99
+ }
100
+ return persistNotice(input.missionDir, notice);
101
+ }
102
+ /**
103
+ * Upgrade zellij to the latest stable release. Only Homebrew automation is
104
+ * attempted (macOS / Linuxbrew); everything else returns manual_required with
105
+ * the exact operator command, mirroring how the Codex CLI update flow behaves.
106
+ */
107
+ export async function upgradeZellijToLatest(input = {}) {
108
+ const before = await checkZellijCapability({ require: false, writeReport: false }).catch(() => null);
109
+ const beforeVersion = before?.version || null;
110
+ const missing = before?.status === 'missing' || !before;
111
+ const latest = await fetchLatestZellijVersion({ env: input.env || process.env });
112
+ const brew = await runProcess('brew', ['--version'], { timeoutMs: 8000, maxOutputBytes: 4096 });
113
+ if (brew.code !== 0) {
114
+ return {
115
+ status: 'manual_required',
116
+ before_version: beforeVersion,
117
+ after_version: beforeVersion,
118
+ latest_version: latest.version,
119
+ command: zellijUpgradeCommandHint(missing),
120
+ error: 'homebrew_not_available'
121
+ };
122
+ }
123
+ const upgradeArgs = missing ? ['install', 'zellij'] : ['upgrade', 'zellij'];
124
+ // Package installs go through the mutation guard with an explicit
125
+ // zellij_install scope contract (same path ensureZellijCliTool uses), so the
126
+ // mutation ledger records the install and safety gates can audit it. The
127
+ // upgrade only runs after the operator confirmed the [Y/n] prompt or passed
128
+ // --yes / `sks zellij update --yes`.
129
+ const guardRoot = globalSksRoot();
130
+ const guardCommand = `brew ${upgradeArgs.join(' ')}`;
131
+ const contract = createRequestedScopeContract({
132
+ route: 'zellij-update',
133
+ userRequest: guardCommand,
134
+ projectRoot: guardRoot,
135
+ overrides: { package_install: true, zellij_install: true }
136
+ });
137
+ const guardCtx = guardContextForRoute(guardRoot, contract, guardCommand);
138
+ let run = await guardedPackageInstall(guardCtx, 'zellij', {
139
+ confirmed: true,
140
+ command: 'brew',
141
+ args: upgradeArgs,
142
+ timeoutMs: input.timeoutMs || 180000,
143
+ maxOutputBytes: 256 * 1024
144
+ }).catch((err) => ({ code: 1, stdout: '', stderr: String(err?.message || err) }));
145
+ if (run.code !== 0 && !missing && /No such keg|No available formula|not installed/i.test(`${run.stderr}\n${run.stdout}`)) {
146
+ // zellij exists on PATH but was not installed through Homebrew (e.g. cargo
147
+ // or a manual binary). Installing the brew formula gives a managed copy.
148
+ run = await guardedPackageInstall(guardCtx, 'zellij', {
149
+ confirmed: true,
150
+ command: 'brew',
151
+ args: ['install', 'zellij'],
152
+ timeoutMs: input.timeoutMs || 180000,
153
+ maxOutputBytes: 256 * 1024
154
+ }).catch((err) => ({ code: 1, stdout: '', stderr: String(err?.message || err) }));
155
+ }
156
+ if (run.code !== 0 && /already installed|already up-to-date/i.test(`${run.stderr}\n${run.stdout}`)) {
157
+ return {
158
+ status: 'noop',
159
+ before_version: beforeVersion,
160
+ after_version: beforeVersion,
161
+ latest_version: latest.version,
162
+ command: `brew ${upgradeArgs.join(' ')}`,
163
+ error: null
164
+ };
165
+ }
166
+ if (run.code !== 0) {
167
+ return {
168
+ status: 'failed',
169
+ before_version: beforeVersion,
170
+ after_version: beforeVersion,
171
+ latest_version: latest.version,
172
+ command: `brew ${upgradeArgs.join(' ')}`,
173
+ error: `${run.stderr || run.stdout || 'brew upgrade failed'}`.trim().slice(-1000)
174
+ };
175
+ }
176
+ const after = await checkZellijCapability({ require: false, writeReport: false }).catch(() => null);
177
+ return {
178
+ status: missing ? 'installed' : 'upgraded',
179
+ before_version: beforeVersion,
180
+ after_version: after?.version || null,
181
+ latest_version: latest.version,
182
+ command: `brew ${upgradeArgs.join(' ')}`,
183
+ error: null
184
+ };
185
+ }
186
+ /**
187
+ * Launch-time prompt, mirroring maybePromptCodexUpdateForLaunch: check the
188
+ * installed zellij version against the latest stable release and offer an
189
+ * upgrade before opening the live session. Never blocks the launch.
190
+ *
191
+ * Skips: --json, --skip-cli-tools, --skip-zellij-update, SKS_SKIP_ZELLIJ_UPDATE=1.
192
+ * Auto-approves: --yes / -y.
193
+ */
194
+ export async function maybePromptZellijUpdateForLaunch(args = [], opts = {}) {
195
+ const env = opts.env || process.env;
196
+ const list = (args || []).map((arg) => String(arg));
197
+ if (list.includes('--json') || list.includes('--skip-cli-tools') || list.includes('--skip-zellij-update') || env.SKS_SKIP_ZELLIJ_UPDATE === '1') {
198
+ return { status: 'skipped', current: null, latest: null, command: null };
199
+ }
200
+ const notice = await checkZellijUpdateNotice({ env }).catch(() => null);
201
+ if (!notice)
202
+ return { status: 'skipped', current: null, latest: null, command: null };
203
+ if (notice.zellij_missing) {
204
+ // Zellij is an optional integration; installation is owned by
205
+ // `sks deps check --yes` / `sks zellij update --yes`. Just surface the hint.
206
+ console.log(`Zellij not found (optional live panes disabled). Install with: ${notice.upgrade_command}`);
207
+ return { status: 'missing', current: null, latest: notice.latest_version, command: notice.upgrade_command };
208
+ }
209
+ if (!notice.update_available) {
210
+ return { status: 'current', current: notice.current_version, latest: notice.latest_version, command: null, error: notice.error || null };
211
+ }
212
+ const label = opts.label || 'Zellij launch';
213
+ const autoYes = list.includes('--yes') || list.includes('-y');
214
+ if (!autoYes && !canAskYesNo(env)) {
215
+ console.log(`Zellij update available: ${notice.current_version} -> ${notice.latest_version}. Run: ${notice.upgrade_command}`);
216
+ return { status: 'available', current: notice.current_version, latest: notice.latest_version, command: notice.upgrade_command };
217
+ }
218
+ if (!autoYes) {
219
+ const yes = await askYesNoDefaultYes(`Zellij ${notice.current_version} -> ${notice.latest_version} update before ${label}? [Y/n] `);
220
+ if (!yes)
221
+ return { status: 'skipped_by_user', current: notice.current_version, latest: notice.latest_version, command: notice.upgrade_command };
222
+ }
223
+ const upgraded = await upgradeZellijToLatest({ env });
224
+ if (upgraded.status === 'upgraded' || upgraded.status === 'installed') {
225
+ console.log(`Zellij ${upgraded.before_version || 'unknown'} -> ${upgraded.after_version || upgraded.latest_version || 'latest'} (${upgraded.command})`);
226
+ }
227
+ else if (upgraded.status === 'manual_required') {
228
+ console.log(`Zellij upgrade needs a manual step: ${upgraded.command}`);
229
+ }
230
+ else if (upgraded.status === 'failed') {
231
+ console.log(`Zellij upgrade failed (launch continues): ${upgraded.error || upgraded.command}`);
232
+ }
233
+ return {
234
+ status: upgraded.status,
235
+ current: upgraded.after_version || upgraded.before_version,
236
+ latest: upgraded.latest_version,
237
+ command: upgraded.command,
238
+ error: upgraded.error || null
239
+ };
240
+ }
241
+ export function zellijUpdateCachePath() {
242
+ return path.join(os.homedir(), '.sneakoscope', 'cache', 'zellij-update-notice.json');
243
+ }
244
+ async function persistNotice(missionDir, notice) {
245
+ if (missionDir)
246
+ await writeJsonAtomic(path.join(missionDir, 'zellij-update-notice.json'), notice).catch(() => undefined);
247
+ return notice;
248
+ }
249
+ function githubLatestTag(timeoutMs) {
250
+ return new Promise((resolve, reject) => {
251
+ const req = https.request({
252
+ hostname: 'api.github.com',
253
+ path: ZELLIJ_RELEASES_API_PATH,
254
+ method: 'GET',
255
+ timeout: timeoutMs,
256
+ headers: {
257
+ Accept: 'application/vnd.github+json',
258
+ 'User-Agent': 'sneakoscope-cli'
259
+ }
260
+ }, (res) => {
261
+ let body = '';
262
+ res.setEncoding('utf8');
263
+ res.on('data', (chunk) => { body += chunk; });
264
+ res.on('end', () => {
265
+ try {
266
+ if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300)
267
+ throw new Error(`github_status_${res.statusCode}`);
268
+ const parsed = JSON.parse(body);
269
+ const tag = String(parsed?.tag_name || '').trim();
270
+ if (!tag)
271
+ throw new Error('github_latest_tag_missing');
272
+ resolve(tag);
273
+ }
274
+ catch (err) {
275
+ reject(err);
276
+ }
277
+ });
278
+ });
279
+ req.on('timeout', () => {
280
+ req.destroy(new Error('zellij_update_check_timeout'));
281
+ });
282
+ req.on('error', reject);
283
+ req.end();
284
+ });
285
+ }
286
+ function canAskYesNo(env) {
287
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && env.CI !== 'true');
288
+ }
289
+ async function askYesNoDefaultYes(question) {
290
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
291
+ try {
292
+ const answer = await new Promise((resolve) => rl.question(question, resolve));
293
+ const trimmed = String(answer || '').trim();
294
+ return trimmed === '' || /^(y|yes|예|네|응)$/i.test(trimmed);
295
+ }
296
+ finally {
297
+ rl.close();
298
+ }
299
+ }
300
+ function normalizePositiveInt(value, fallback) {
301
+ const parsed = Number(value);
302
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
303
+ }
304
+ function stripVersionPrefix(value) {
305
+ return value.replace(/^v(?=\d)/i, '');
306
+ }
307
+ //# sourceMappingURL=zellij-update.js.map