sneakoscope 0.7.74 → 0.7.78
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 +6 -1
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +178 -3
- package/src/cli/main.mjs +166 -27
- package/src/cli/maintenance-commands.mjs +40 -8
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +24 -8
- package/src/core/init.mjs +12 -17
- package/src/core/pipeline.mjs +6 -1
- package/src/core/routes.mjs +3 -3
- package/src/core/team-live.mjs +81 -12
- package/src/core/tmux-ui.mjs +303 -61
- package/src/core/version-manager.mjs +61 -31
package/README.md
CHANGED
|
@@ -150,11 +150,12 @@ For [codex-lb](https://github.com/Soju06/codex-lb), start the server, create a d
|
|
|
150
150
|
|
|
151
151
|
```sh
|
|
152
152
|
sks codex-lb setup --host https://your-codex-lb.example.com --api-key "sk-clb-..."
|
|
153
|
+
sks codex-lb health
|
|
153
154
|
sks codex-lb repair
|
|
154
155
|
sks
|
|
155
156
|
```
|
|
156
157
|
|
|
157
|
-
Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, syncs `codex login --with-api-key`, and loads it in tmux.
|
|
158
|
+
Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, syncs `codex login --with-api-key`, and loads it in tmux. When codex-lb is active, SKS opens a fresh `sks-codex-lb-*` tmux session and sweeps older detached codex-lb sessions for the same repo before launch so stale Responses API chains are not reused. Configured launch paths, including non-interactive runs, verify that codex-lb can continue a Responses API chain with `previous_response_id`; if that check fails, SKS bypasses codex-lb for that launch with `model_provider="openai"` instead of letting the Codex session fail mid-work.
|
|
158
159
|
|
|
159
160
|
If Codex CLI auth drifts after launch/reinstall, run `sks doctor --fix` or `sks codex-lb repair`; to replace it, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
|
|
160
161
|
|
|
@@ -278,6 +279,8 @@ Default setup adds these generated SKS paths to the project `.gitignore`; `--loc
|
|
|
278
279
|
|
|
279
280
|
Use `sks dollar-commands` to confirm that terminal discovery and Codex App prompt commands agree.
|
|
280
281
|
|
|
282
|
+
SKS does not install Git pre-commit hooks. Release metadata is changed only by explicit commands such as `sks versioning bump`, and `sks versioning hook` is intentionally blocked so Codex App commit/push flows stay unobstructed.
|
|
283
|
+
|
|
281
284
|
TriWiki is intentionally sparse: `sks wiki sweep` records demote, soft-forget, archive, delete, promote-to-skill, and promote-to-rule candidates instead of injecting every old claim into future prompts. `sks harness fixture` validates the broader Harness Growth Factory contract: deliberate forgetting fixtures, skill card metadata, experiment schema, tool-error taxonomy, permission profiles, MultiAgentV2 defaults, and tmux cockpit view coverage. `sks code-structure scan` flags handwritten files above 1000/2000/3000-line thresholds so new logic can be extracted before command files become harder to maintain.
|
|
282
285
|
|
|
283
286
|
## OpenClaw Agent Usage
|
|
@@ -416,6 +419,8 @@ npm run publish:dry
|
|
|
416
419
|
|
|
417
420
|
`release:check` runs audit, changelog, syntax, selftest, size, and registry checks. `publish:dry` runs that same gate and then performs an npm dry-run publish against the public registry.
|
|
418
421
|
|
|
422
|
+
Version bumps are manual. Run `sks versioning bump` only when preparing release metadata; SKS will not create `.git/hooks/pre-commit` or auto-bump during ordinary commits.
|
|
423
|
+
|
|
419
424
|
## License
|
|
420
425
|
|
|
421
426
|
MIT
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.78",
|
|
5
5
|
"description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -217,6 +217,126 @@ export async function codexLbStatus(opts = {}) {
|
|
|
217
217
|
};
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
function codexLbResponsesEndpoint(baseUrl = '') {
|
|
221
|
+
const base = String(baseUrl || '').trim().replace(/\/+$/, '');
|
|
222
|
+
if (!base) return '';
|
|
223
|
+
return /\/responses$/i.test(base) ? base : `${base}/responses`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function codexLbChainCheckEnabled(env = process.env) {
|
|
227
|
+
return env.SKS_CODEX_LB_CHAIN_CHECK !== '0' && env.SKS_SKIP_CODEX_LB_CHAIN_CHECK !== '1';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isPreviousResponseNotFound(payload = {}) {
|
|
231
|
+
const error = payload?.error || payload?.response?.error || payload;
|
|
232
|
+
const text = typeof error === 'string'
|
|
233
|
+
? error
|
|
234
|
+
: [error?.type, error?.code, error?.message, error?.param, JSON.stringify(error || {})].filter(Boolean).join(' ');
|
|
235
|
+
return /previous_response_not_found|previous_response_id.*not found|previous_response_id/i.test(text);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function parseCodexLbSseEvents(text = '') {
|
|
239
|
+
const events = [];
|
|
240
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
241
|
+
if (!line.startsWith('data:')) continue;
|
|
242
|
+
const data = line.slice(5).trim();
|
|
243
|
+
if (!data || data === '[DONE]') continue;
|
|
244
|
+
try {
|
|
245
|
+
events.push(JSON.parse(data));
|
|
246
|
+
} catch {}
|
|
247
|
+
}
|
|
248
|
+
return events;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function codexLbResponseId(payload = {}) {
|
|
252
|
+
if (typeof payload?.id === 'string' && payload.id) return payload.id;
|
|
253
|
+
if (typeof payload?.response?.id === 'string' && payload.response.id) return payload.response.id;
|
|
254
|
+
if (typeof payload?.data?.id === 'string' && payload.data.id) return payload.data.id;
|
|
255
|
+
if (typeof payload?.data?.response?.id === 'string' && payload.data.response.id) return payload.data.response.id;
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function codexLbResponseError(json, events = []) {
|
|
260
|
+
if (json?.error) return json;
|
|
261
|
+
for (const event of events) {
|
|
262
|
+
if (event?.error || event?.response?.error || event?.type === 'response.failed' || event?.type === 'error') return event;
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function fetchCodexLbResponse(fetchImpl, endpoint, apiKey, body, timeoutMs) {
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs).unref?.();
|
|
270
|
+
try {
|
|
271
|
+
const response = await fetchImpl(endpoint, {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: {
|
|
274
|
+
authorization: `Bearer ${apiKey}`,
|
|
275
|
+
'content-type': 'application/json'
|
|
276
|
+
},
|
|
277
|
+
body: JSON.stringify(body),
|
|
278
|
+
signal: controller.signal
|
|
279
|
+
});
|
|
280
|
+
const text = await response.text();
|
|
281
|
+
let json = null;
|
|
282
|
+
try { json = text ? JSON.parse(text) : null; } catch {}
|
|
283
|
+
const events = json ? [] : parseCodexLbSseEvents(text);
|
|
284
|
+
const responseId = codexLbResponseId(json) || events.map((event) => codexLbResponseId(event)).find(Boolean) || null;
|
|
285
|
+
const errorPayload = codexLbResponseError(json, events);
|
|
286
|
+
return { ok: response.ok && !errorPayload, status: response.status, json, text, events, response_id: responseId, error_payload: errorPayload };
|
|
287
|
+
} catch (err) {
|
|
288
|
+
return { ok: false, status: 0, json: null, text: err.name === 'AbortError' ? 'request timed out' : err.message, events: [], response_id: null, error_payload: null };
|
|
289
|
+
} finally {
|
|
290
|
+
clearTimeout(timer);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function checkCodexLbResponseChain(status = {}, opts = {}) {
|
|
295
|
+
const env = opts.env || process.env;
|
|
296
|
+
if (!codexLbChainCheckEnabled(env) && !opts.force) return { ok: true, status: 'skipped', skipped: true, reason: 'SKS_CODEX_LB_CHAIN_CHECK=0' };
|
|
297
|
+
const endpoint = codexLbResponsesEndpoint(opts.baseUrl || status.base_url);
|
|
298
|
+
if (!endpoint) return { ok: false, status: 'missing_base_url', chain_unhealthy: true };
|
|
299
|
+
const apiKey = opts.apiKey || parseCodexLbEnvKey(await readText(opts.envPath || status.env_path || codexLbEnvPath(opts.home || env.HOME || os.homedir()), ''));
|
|
300
|
+
if (!apiKey) return { ok: false, status: 'missing_env_key', chain_unhealthy: true };
|
|
301
|
+
const fetchImpl = opts.fetch || globalThis.fetch;
|
|
302
|
+
if (typeof fetchImpl !== 'function') return { ok: true, status: 'skipped', skipped: true, reason: 'fetch unavailable' };
|
|
303
|
+
const model = opts.model || env.SKS_CODEX_MODEL || 'gpt-5.5';
|
|
304
|
+
const timeoutMs = Number(opts.timeoutMs || env.SKS_CODEX_LB_CHAIN_CHECK_TIMEOUT_MS || 8000);
|
|
305
|
+
const baseBody = {
|
|
306
|
+
model,
|
|
307
|
+
instructions: 'You are running a short SKS codex-lb response-chain health check.',
|
|
308
|
+
input: 'SKS codex-lb response-chain health check. Reply with OK.',
|
|
309
|
+
stream: true,
|
|
310
|
+
store: true,
|
|
311
|
+
parallel_tool_calls: false,
|
|
312
|
+
tool_choice: 'auto',
|
|
313
|
+
reasoning: { effort: 'low' }
|
|
314
|
+
};
|
|
315
|
+
const first = await fetchCodexLbResponse(fetchImpl, endpoint, apiKey, baseBody, timeoutMs);
|
|
316
|
+
if (!first.ok || !first.response_id) {
|
|
317
|
+
return {
|
|
318
|
+
ok: false,
|
|
319
|
+
status: first.ok ? 'missing_response_id' : 'first_request_failed',
|
|
320
|
+
chain_unhealthy: true,
|
|
321
|
+
endpoint,
|
|
322
|
+
http_status: first.status,
|
|
323
|
+
error: redactSecretText(first.error_payload?.error?.message || first.error_payload?.response?.error?.message || first.text || 'codex-lb first Responses request failed', [apiKey])
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const second = await fetchCodexLbResponse(fetchImpl, endpoint, apiKey, { ...baseBody, previous_response_id: first.response_id }, timeoutMs);
|
|
327
|
+
if (second.ok) return { ok: true, status: 'chain_ok', endpoint, response_id: first.response_id, chained_response_id: second.response_id || null, http_status: second.status };
|
|
328
|
+
const previousMissing = isPreviousResponseNotFound(second.error_payload || second.json || second.text);
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
status: previousMissing ? 'previous_response_not_found' : 'second_request_failed',
|
|
332
|
+
chain_unhealthy: true,
|
|
333
|
+
endpoint,
|
|
334
|
+
response_id: first.response_id,
|
|
335
|
+
http_status: second.status,
|
|
336
|
+
error: redactSecretText(second.error_payload?.error?.message || second.error_payload?.response?.error?.message || second.text || 'codex-lb chained Responses request failed', [apiKey])
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
220
340
|
function hasTopLevelCodexLbSelected(text = '') {
|
|
221
341
|
const topLevel = String(text || '').split(/\n\s*\[/)[0] || '';
|
|
222
342
|
return /(^|\n)\s*model_provider\s*=\s*"codex-lb"\s*(?:#.*)?(?=\n|$)/.test(topLevel);
|
|
@@ -283,13 +403,18 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
283
403
|
|
|
284
404
|
export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
|
|
285
405
|
if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
|
|
286
|
-
if (!canAskYesNo()) return { status: 'non_interactive' };
|
|
287
406
|
const status = await codexLbStatus(opts);
|
|
288
407
|
if (status.ok) {
|
|
289
408
|
const codexLogin = await ensureCodexLbLoginFromEnv(status, opts);
|
|
290
409
|
if (codexLogin.status === 'synced') console.log('codex-lb auth synced with Codex CLI.');
|
|
291
|
-
|
|
410
|
+
const chainHealth = await checkCodexLbResponseChain(status, opts);
|
|
411
|
+
if (!chainHealth.ok && chainHealth.chain_unhealthy) {
|
|
412
|
+
console.log(`codex-lb response chain check failed (${chainHealth.status}); bypassing codex-lb for this launch.`);
|
|
413
|
+
return { status: 'chain_unhealthy', ...status, ok: false, codex_login: codexLogin, chain_health: chainHealth, bypass_codex_lb: true };
|
|
414
|
+
}
|
|
415
|
+
return { status: 'present', ...status, codex_login: codexLogin, chain_health: chainHealth };
|
|
292
416
|
}
|
|
417
|
+
if (!canAskYesNo()) return { status: 'non_interactive', codex_lb: status };
|
|
293
418
|
const useCodexLb = (await askPostinstallQuestion('\nAuthenticate and route Codex through codex-lb? [y/N] ')).trim();
|
|
294
419
|
if (!/^(y|yes|예|네|응)$/i.test(useCodexLb)) return { status: 'continued_to_codex' };
|
|
295
420
|
const host = (await askPostinstallQuestion('codex-lb host domain [http://127.0.0.1:2455]: ')).trim() || 'http://127.0.0.1:2455';
|
|
@@ -1110,6 +1235,56 @@ export async function selftestCodexLb(tmp) {
|
|
|
1110
1235
|
if (codexLbNotConfigured.code !== 0 || String(codexLbNotConfigured.stdout || '').includes('codex-lb auth:')) throw new Error('selftest: postinstall should stay quiet when codex-lb is not configured');
|
|
1111
1236
|
const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1112
1237
|
if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise repair command');
|
|
1238
|
+
const nonInteractiveLaunchChainCalls = [];
|
|
1239
|
+
const nonInteractiveLaunch = await maybePromptCodexLbSetupForLaunch([], {
|
|
1240
|
+
home: codexLbHome,
|
|
1241
|
+
apiKey: 'sk-test',
|
|
1242
|
+
codexBin: path.join(codexLbFakeBin, 'codex'),
|
|
1243
|
+
timeoutMs: 1000,
|
|
1244
|
+
fetch: async (url, init) => {
|
|
1245
|
+
nonInteractiveLaunchChainCalls.push({ url, body: JSON.parse(init.body) });
|
|
1246
|
+
return new Response(JSON.stringify({ id: nonInteractiveLaunchChainCalls.length === 1 ? 'resp_noninteractive_1' : 'resp_noninteractive_2' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
if (!nonInteractiveLaunch.ok || nonInteractiveLaunch.status !== 'present' || nonInteractiveLaunch.chain_health?.status !== 'chain_ok' || nonInteractiveLaunchChainCalls.length !== 2 || nonInteractiveLaunchChainCalls[1].body.previous_response_id !== 'resp_noninteractive_1') throw new Error('selftest: non-interactive codex-lb launch path did not run response-chain preflight');
|
|
1250
|
+
const nonInteractiveBrokenLaunch = await maybePromptCodexLbSetupForLaunch([], {
|
|
1251
|
+
home: codexLbHome,
|
|
1252
|
+
apiKey: 'sk-test',
|
|
1253
|
+
codexBin: path.join(codexLbFakeBin, 'codex'),
|
|
1254
|
+
timeoutMs: 1000,
|
|
1255
|
+
fetch: async (_url, init) => {
|
|
1256
|
+
const body = JSON.parse(init.body);
|
|
1257
|
+
if (!body.previous_response_id) return new Response(JSON.stringify({ id: 'resp_noninteractive_broken' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
1258
|
+
return new Response(JSON.stringify({ error: { type: 'invalid_request_error', code: 'previous_response_not_found', message: 'Previous response not found.', param: 'previous_response_id' } }), { status: 400, headers: { 'content-type': 'application/json' } });
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
if (nonInteractiveBrokenLaunch.status !== 'chain_unhealthy' || nonInteractiveBrokenLaunch.bypass_codex_lb !== true || nonInteractiveBrokenLaunch.chain_health?.status !== 'previous_response_not_found') throw new Error('selftest: non-interactive codex-lb launch path did not bypass on previous_response_not_found');
|
|
1262
|
+
const chainCalls = [];
|
|
1263
|
+
const okChain = await checkCodexLbResponseChain(
|
|
1264
|
+
{ base_url: 'https://lb.example.test/backend-api/codex', env_path: path.join(codexLbHome, '.codex', 'sks-codex-lb.env') },
|
|
1265
|
+
{
|
|
1266
|
+
apiKey: 'sk-test',
|
|
1267
|
+
timeoutMs: 1000,
|
|
1268
|
+
fetch: async (url, init) => {
|
|
1269
|
+
chainCalls.push({ url, body: JSON.parse(init.body) });
|
|
1270
|
+
return new Response(JSON.stringify({ id: chainCalls.length === 1 ? 'resp_selftest_1' : 'resp_selftest_2' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
);
|
|
1274
|
+
if (!okChain.ok || okChain.status !== 'chain_ok' || chainCalls.length !== 2 || !String(chainCalls[0].url).endsWith('/backend-api/codex/responses') || chainCalls[1].body.previous_response_id !== 'resp_selftest_1') throw new Error('selftest: codex-lb response chain health check did not verify previous_response_id continuity');
|
|
1275
|
+
const brokenChain = await checkCodexLbResponseChain(
|
|
1276
|
+
{ base_url: 'https://lb.example.test/backend-api/codex', env_path: path.join(codexLbHome, '.codex', 'sks-codex-lb.env') },
|
|
1277
|
+
{
|
|
1278
|
+
apiKey: 'sk-test',
|
|
1279
|
+
timeoutMs: 1000,
|
|
1280
|
+
fetch: async (_url, init) => {
|
|
1281
|
+
const body = JSON.parse(init.body);
|
|
1282
|
+
if (!body.previous_response_id) return new Response(JSON.stringify({ id: 'resp_missing_selftest' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
1283
|
+
return new Response(JSON.stringify({ error: { type: 'invalid_request_error', code: 'previous_response_not_found', message: 'Previous response not found.', param: 'previous_response_id' } }), { status: 400, headers: { 'content-type': 'application/json' } });
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
);
|
|
1287
|
+
if (brokenChain.ok || brokenChain.status !== 'previous_response_not_found' || brokenChain.chain_unhealthy !== true) throw new Error('selftest: codex-lb response chain health check did not detect previous_response_not_found');
|
|
1113
1288
|
if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('hooks = true') || hasDeprecatedCodexHooksFeatureFlag(codexLbConfig) || !codexLbConfig.includes('multi_agent = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('codex_git_commit = true') || !codexLbConfig.includes('computer_use = true') || !codexLbConfig.includes('apps = true') || !codexLbConfig.includes('plugins = true') || !codexLbConfig.includes('[user.fast_mode]') || !codexLbConfig.includes('visible = true') || !codexLbConfig.includes('enabled = true') || !codexLbConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(codexLbConfig) || codexLbConfig.includes('fast_default_opt_out = true') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest: codex-lb setup did not preserve Codex App feature flags, Fast mode defaults, Codex Git commit generation, force GPT-5.5, or migrate the hooks feature flag');
|
|
1114
1289
|
if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig)) throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
|
|
1115
1290
|
const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
|
|
@@ -1117,7 +1292,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1117
1292
|
if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest: tmux launch command without args did not force GPT-5.5');
|
|
1118
1293
|
if (!codexLbLaunch.includes('SKS_TMUX_LOGO_ANIMATION') || !codexLbLaunch.includes('SNEAKOSCOPE CODEX')) throw new Error('selftest: tmux launch command does not include the animated SKS logo intro');
|
|
1119
1294
|
const madLaunchSource = await safeReadText(path.join(packageRoot(), 'src', 'cli', 'maintenance-commands.mjs'));
|
|
1120
|
-
if (!madLaunchSource.includes('const lb = await deps.maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb')) throw new Error('selftest: MAD launch does not sync codex-lb auth and fresh-session launch options');
|
|
1295
|
+
if (!madLaunchSource.includes('const lb = await deps.maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb') || !madLaunchSource.includes('bypass_codex_lb') || !madLaunchSource.includes('model_provider="openai"') || !madLaunchSource.includes('codexLbFreshSession: true')) throw new Error('selftest: MAD launch does not sync codex-lb auth and fresh-session launch options');
|
|
1121
1296
|
|
|
1122
1297
|
}
|
|
1123
1298
|
|
package/src/cli/main.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { classifySql, classifyCommand, classifyToolPayload, checkDbOperation, ha
|
|
|
18
18
|
import { checkHarnessModification, harnessGuardStatus, isHarnessSourceProject } from '../core/harness-guard.mjs';
|
|
19
19
|
import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
|
|
20
20
|
import { context7Docs, context7Resolve, context7Text, context7Tools } from '../core/context7-client.mjs';
|
|
21
|
-
import { bumpProjectVersion,
|
|
21
|
+
import { bumpProjectVersion, disableVersionGitHook, runVersionPreCommit, versioningStatus } from '../core/version-manager.mjs';
|
|
22
22
|
import { rustInfo } from '../core/rust-accelerator.mjs';
|
|
23
23
|
import { renderCartridge, validateCartridge, driftCartridge, snapshotCartridge } from '../core/gx-renderer.mjs';
|
|
24
24
|
import { defaultEvaluationScenario, runEvaluationBenchmark } from '../core/evaluation.mjs';
|
|
@@ -75,10 +75,10 @@ import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
|
|
|
75
75
|
import { CODEX_APP_DOCS_URL, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
|
|
76
76
|
import { codexAppRemoteControlCommand } from './codex-app-command.mjs';
|
|
77
77
|
import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs';
|
|
78
|
-
import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, reconcileTmuxTeamCockpit, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
|
|
78
|
+
import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, reconcileTmuxTeamCockpit, runTmuxStatus, sanitizeTmuxSessionName, sweepCodexLbTmuxSessions, sweepTmuxTeamSurfaces, teamLaneStyle } from '../core/tmux-ui.mjs';
|
|
79
79
|
import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
|
|
80
80
|
import { context7Command } from './context7-command.mjs';
|
|
81
|
-
import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall } from './install-helpers.mjs';
|
|
81
|
+
import { askPostinstallQuestion, checkCodexLbResponseChain, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall } from './install-helpers.mjs';
|
|
82
82
|
import { buildTeamPlan, codeStructureCommand, dbCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, harnessCommand, hproofCommand, madHighCommand as runMadHighCommand, memoryCommand, migrateWikiContextPack, parseTeamCreateArgs, perfCommand, profileCommand, projectWikiClaims, proofFieldCommand, qaLoopCommand, quickstartCommand, researchCommand, skillDreamCommand, statsCommand, team, teamWorkflowMarkdown, validateArtifactsCommand, wikiCommand, wikiVoxelRowCount, writeWikiContextPack } from './maintenance-commands.mjs';
|
|
83
83
|
import { openClawCommand } from './openclaw-command.mjs';
|
|
84
84
|
|
|
@@ -144,13 +144,20 @@ async function defaultTmuxCommand(args = []) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
function codexLbImmediateLaunchOpts(args = [], lb = {}, opts = {}) {
|
|
147
|
-
if (!lb?.ok || lb.status !== 'configured') return opts;
|
|
148
|
-
if (readOption(args, '--session', null) || readOption(args, '--workspace', null)) return opts;
|
|
149
147
|
const root = readOption(args, '--root', process.cwd());
|
|
148
|
+
const explicitSession = readOption(args, '--session', null) || readOption(args, '--workspace', null);
|
|
149
|
+
if (lb?.bypass_codex_lb) {
|
|
150
|
+
const session = explicitSession || sanitizeTmuxSessionName(`sks-openai-fallback-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
|
|
151
|
+
console.log(`codex-lb bypass active for this launch: ${lb.chain_health?.status || lb.status}`);
|
|
152
|
+
console.log(`Using fresh OpenAI fallback tmux session: ${session}`);
|
|
153
|
+
return { ...opts, session, codexArgs: [...(opts.codexArgs || []), '-c', 'model_provider="openai"'], codexLbBypassed: true };
|
|
154
|
+
}
|
|
155
|
+
if (!lb?.ok) return opts;
|
|
156
|
+
if (explicitSession) return opts;
|
|
150
157
|
const session = sanitizeTmuxSessionName(`sks-codex-lb-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
|
|
151
|
-
console.log(`codex-lb
|
|
158
|
+
console.log(`codex-lb active for this launch: ${lb.env_path || lb.base_url || 'configured'}`);
|
|
152
159
|
console.log(`Using fresh tmux session: ${session}`);
|
|
153
|
-
return { ...opts, session };
|
|
160
|
+
return { ...opts, session, codexLbFreshSession: true };
|
|
154
161
|
}
|
|
155
162
|
|
|
156
163
|
function help(args = []) {
|
|
@@ -171,8 +178,8 @@ Usage:
|
|
|
171
178
|
sks bootstrap [--install-scope global|project] [--local-only] [--json]
|
|
172
179
|
sks deps check|install [tmux|codex|context7|all] [--yes] [--json]
|
|
173
180
|
sks codex-app
|
|
174
|
-
sks codex-lb status|repair|setup --host <domain> --api-key <key>
|
|
175
|
-
sks auth status|repair|setup --host <domain> --api-key <key>
|
|
181
|
+
sks codex-lb status|health|repair|setup --host <domain> --api-key <key>
|
|
182
|
+
sks auth status|health|repair|setup --host <domain> --api-key <key>
|
|
176
183
|
sks openclaw install|path|print [--dir path] [--force] [--json]
|
|
177
184
|
sks --mad [--high]
|
|
178
185
|
sks auto-review status|enable|start [--high]
|
|
@@ -192,7 +199,7 @@ Usage:
|
|
|
192
199
|
sks pipeline answer <mission-id|latest> <answers.json|--stdin|--text "...">
|
|
193
200
|
sks guard check [--json]
|
|
194
201
|
sks conflicts check|prompt [--json]
|
|
195
|
-
sks versioning status|bump|
|
|
202
|
+
sks versioning status|bump|disable [--json]
|
|
196
203
|
sks reasoning ["prompt"] [--json]
|
|
197
204
|
sks aliases
|
|
198
205
|
sks setup [--bootstrap] [--install-scope global|project] [--local-only] [--force] [--json]
|
|
@@ -1002,10 +1009,19 @@ async function versioning(sub = 'status', args = []) {
|
|
|
1002
1009
|
if (!status.ok) console.log('Run: sks doctor --fix');
|
|
1003
1010
|
return;
|
|
1004
1011
|
}
|
|
1005
|
-
if (action === 'hook' || action === 'install-hook') {
|
|
1006
|
-
const res = await
|
|
1012
|
+
if (action === 'hook' || action === 'install-hook' || action === 'enable') {
|
|
1013
|
+
const res = await disableVersionGitHook(root);
|
|
1014
|
+
const blocked = { ...res, ok: false, installed: false, reason: 'pre_commit_hooks_unsupported' };
|
|
1015
|
+
process.exitCode = 2;
|
|
1016
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(blocked, null, 2));
|
|
1017
|
+
console.error('SKS no longer installs Git pre-commit hooks. Use `sks versioning bump` and release checks explicitly.');
|
|
1018
|
+
if (res.hook_removed) console.error(`Removed existing SKS version hook: ${res.hook_path}`);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (action === 'disable' || action === 'off' || action === 'remove-hook' || action === 'unhook') {
|
|
1022
|
+
const res = await disableVersionGitHook(root);
|
|
1007
1023
|
if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
|
|
1008
|
-
console.log(res.
|
|
1024
|
+
console.log(res.hook_removed ? `Version hook removed: ${res.hook_path}` : `Version hook disabled: ${res.reason || 'policy updated'}`);
|
|
1009
1025
|
return;
|
|
1010
1026
|
}
|
|
1011
1027
|
if (action === 'bump') {
|
|
@@ -1032,7 +1048,7 @@ async function versioning(sub = 'status', args = []) {
|
|
|
1032
1048
|
console.log(res.changed ? `SKS versioning synced: ${res.version}` : `SKS versioning: ${res.version} verified`);
|
|
1033
1049
|
return;
|
|
1034
1050
|
}
|
|
1035
|
-
console.error('Usage: sks versioning status|bump|
|
|
1051
|
+
console.error('Usage: sks versioning status|bump|disable [--json]');
|
|
1036
1052
|
process.exitCode = 1;
|
|
1037
1053
|
}
|
|
1038
1054
|
|
|
@@ -1125,6 +1141,21 @@ async function codexLbCommand(action = 'status', args = []) {
|
|
|
1125
1141
|
else console.log('\nRepair auth: sks codex-lb repair');
|
|
1126
1142
|
return;
|
|
1127
1143
|
}
|
|
1144
|
+
if (sub === 'health' || sub === 'verify-chain' || sub === 'chain') {
|
|
1145
|
+
const status = await codexLbStatus();
|
|
1146
|
+
const result = status.ok
|
|
1147
|
+
? await checkCodexLbResponseChain(status, { force: true })
|
|
1148
|
+
: { ok: false, status: 'not_configured', codex_lb: status };
|
|
1149
|
+
if (json) return console.log(JSON.stringify(result, null, 2));
|
|
1150
|
+
if (result.ok) {
|
|
1151
|
+
console.log('codex-lb response chain: ok');
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
console.error(`codex-lb response chain: failed (${result.status})`);
|
|
1155
|
+
if (result.error) console.error(result.error);
|
|
1156
|
+
process.exitCode = 1;
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1128
1159
|
if (sub === 'repair' || sub === 'resync' || sub === 'login') {
|
|
1129
1160
|
const result = await repairCodexLbAuth();
|
|
1130
1161
|
if (json) return console.log(JSON.stringify(result, null, 2));
|
|
@@ -1160,7 +1191,7 @@ async function codexLbCommand(action = 'status', args = []) {
|
|
|
1160
1191
|
console.log(`Key env: ${result.env_path}`);
|
|
1161
1192
|
return;
|
|
1162
1193
|
}
|
|
1163
|
-
console.error('Usage: sks codex-lb status|repair|setup --host <domain> --api-key <key> [--json]');
|
|
1194
|
+
console.error('Usage: sks codex-lb status|health|repair|setup --host <domain> --api-key <key> [--json]');
|
|
1164
1195
|
process.exitCode = 1;
|
|
1165
1196
|
}
|
|
1166
1197
|
|
|
@@ -1448,7 +1479,7 @@ function usage(args = []) {
|
|
|
1448
1479
|
const topic = String(args[0] || 'overview').toLowerCase();
|
|
1449
1480
|
const blocks = {
|
|
1450
1481
|
overview: [sksAsciiLogo(), '', 'Usage', '', 'Discover:', ' sks commands', ' sks quickstart', ' sks root', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks tmux check', ' sks dollar-commands', '', `Topics: ${USAGE_TOPICS}`],
|
|
1451
|
-
install: ['Install', '', '1. Global install:', ' npm i -g sneakoscope', '', '2. Bootstrap and check dependencies:', ' sks bootstrap', ' sks deps check', '', '3. Confirm Codex App commands:', ' sks codex-app check', ' sks dollar-commands', '', '4. Optional codex-lb key setup for CLI sks runs:', ' sks codex-lb setup --host <domain> --api-key <key>', ' sks codex-lb repair', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
|
|
1482
|
+
install: ['Install', '', '1. Global install:', ' npm i -g sneakoscope', '', '2. Bootstrap and check dependencies:', ' sks bootstrap', ' sks deps check', '', '3. Confirm Codex App commands:', ' sks codex-app check', ' sks dollar-commands', '', '4. Optional codex-lb key setup for CLI sks runs:', ' sks codex-lb setup --host <domain> --api-key <key>', ' sks codex-lb health', ' sks codex-lb repair', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
|
|
1452
1483
|
bootstrap: ['Bootstrap', '', ' sks bootstrap', ' sks setup --bootstrap', '', 'Creates project SKS files, Codex App skills/hooks/config, state/guard files, then checks Codex App, Context7, and tmux.'],
|
|
1453
1484
|
root: ['Root', '', ' sks root [--json]', '', 'Inside a project, SKS uses that project root. Outside any project marker, runtime commands use the per-user global SKS root instead of writing .sneakoscope into the current random folder.'],
|
|
1454
1485
|
deps: ['Dependencies', '', ' sks deps check [--json]', ' sks deps install [tmux|codex|context7|all] [--yes]', '', 'tmux on macOS uses Homebrew after Y/n approval for missing installs or Homebrew-managed upgrades. If PATH resolves an npm-managed tmux, SKS prompts for npm i -g tmux@latest instead. Unknown non-Homebrew tmux paths are reported as conflicts.'],
|
|
@@ -1598,7 +1629,7 @@ async function setup(args) {
|
|
|
1598
1629
|
console.log(`Install: ${install.ok ? 'ok' : 'missing'} ${install.scope} (${install.command_prefix})`);
|
|
1599
1630
|
console.log(`CLI tools: Codex ${formatCodexCliToolStatus(cliTools.codex)}; tmux ${tmuxStatusKind(cliTools.tmux)} ${cliTools.tmux.version || cliTools.tmux.error || ''}`.trimEnd());
|
|
1600
1631
|
console.log(`Hooks: ${path.relative(root, hooksPath)}`);
|
|
1601
|
-
console.log(`Version:
|
|
1632
|
+
console.log(`Version: explicit bump only${versioningInfo.package_version ? ` (${versioningInfo.package_version})` : ''}`);
|
|
1602
1633
|
if (localOnly) console.log('Git: local-only (.git/info/exclude; user AGENTS preserved, SKS managed block refreshed)');
|
|
1603
1634
|
else console.log('Git: .gitignore ignores SKS generated files');
|
|
1604
1635
|
console.log(`Codex App: .codex/config.toml, .codex/hooks.json, .agents/skills, .codex/agents, .codex/SNEAKOSCOPE.md`);
|
|
@@ -1764,7 +1795,7 @@ async function doctor(args) {
|
|
|
1764
1795
|
if (!appRuntime.ok) console.log('Codex App or first-party MCP/plugin tools missing. Run: sks codex-app check');
|
|
1765
1796
|
if (!result.runtime.tmux.ok) console.log('tmux missing. Run: sks deps install tmux');
|
|
1766
1797
|
if (!result.harness_guard.ok) console.log('Harness guard failed. Run: sks setup from a real terminal, then sks guard check.');
|
|
1767
|
-
if (!result.versioning.ok) console.log('Versioning
|
|
1798
|
+
if (!result.versioning.ok) console.log('Versioning metadata drift detected. Run: sks versioning status, then sks versioning bump if release metadata should change.');
|
|
1768
1799
|
if (!result.codex_lb.ready) console.log('codex-lb config/auth drift detected. Run: sks doctor --fix, or reconfigure once with sks codex-lb reconfigure --host <domain> --api-key <key>.');
|
|
1769
1800
|
if (!result.skills.ok) console.log(`Missing skills: ${result.skills.missing.join(', ')}. Run: sks setup`);
|
|
1770
1801
|
if (!result.global_skills.ok) console.log(`Missing global $ skills: ${result.global_skills.missing.join(', ')}. Run: npm i -g sneakoscope, or sks setup from a non-local-only run.`);
|
|
@@ -2373,22 +2404,27 @@ async function selftest() {
|
|
|
2373
2404
|
await writeTextAtomic(path.join(versionTmp, '.git', 'hooks', 'pre-commit'), '#!/bin/sh\nexit 0\n');
|
|
2374
2405
|
await initProject(versionTmp, {});
|
|
2375
2406
|
const versionStatus = await versioningStatus(versionTmp);
|
|
2376
|
-
if (!versionStatus.ok ||
|
|
2377
|
-
|
|
2378
|
-
if (
|
|
2379
|
-
|
|
2407
|
+
if (!versionStatus.ok || versionStatus.enabled || versionStatus.hook_installed) throw new Error('selftest: versioning hook should stay disabled after init');
|
|
2408
|
+
let versionHookText = await safeReadText(versionStatus.hook_path);
|
|
2409
|
+
if (versionHookText.includes('versioning pre-commit')) throw new Error('selftest: init installed versioning pre-commit');
|
|
2410
|
+
const versionHookAttempt = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'versioning', 'hook', '--json'], { cwd: versionTmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2411
|
+
if (versionHookAttempt.code === 0 || !versionHookAttempt.stdout.includes('pre_commit_hooks_unsupported')) throw new Error('selftest: versioning hook command should be blocked');
|
|
2412
|
+
const versionBlockedStatus = await versioningStatus(versionTmp);
|
|
2413
|
+
if (versionBlockedStatus.enabled || versionBlockedStatus.hook_installed) throw new Error('selftest: blocked versioning hook changed status');
|
|
2414
|
+
versionHookText = await safeReadText(versionBlockedStatus.hook_path);
|
|
2415
|
+
if (versionHookText.includes('versioning pre-commit')) throw new Error('selftest: blocked versioning hook installed pre-commit command');
|
|
2380
2416
|
await writeTextAtomic(path.join(versionTmp, 'CHANGELOG.md'), '# Changelog\n\n## [Unreleased]\n\n## [0.1.0] - 2026-05-08\n\n### Fixed\n\n- Initial version selftest fixture.\n');
|
|
2381
2417
|
await writeTextAtomic(path.join(versionTmp, 'README.md'), 'version selftest\n');
|
|
2382
2418
|
await runProcess('git', ['add', 'README.md', 'CHANGELOG.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2383
2419
|
const preCommitVerify = await runVersionPreCommit(versionTmp);
|
|
2384
|
-
if (!preCommitVerify.ok || preCommitVerify.
|
|
2420
|
+
if (!preCommitVerify.ok || !preCommitVerify.skipped || preCommitVerify.reason !== 'disabled_by_policy') throw new Error('selftest: pre-commit path should stay disabled by policy');
|
|
2385
2421
|
const firstVersionBump = await bumpProjectVersion(versionTmp);
|
|
2386
2422
|
if (!firstVersionBump.ok || firstVersionBump.version !== '0.1.1' || !firstVersionBump.changed) throw new Error('selftest: first version bump did not advance patch version');
|
|
2387
2423
|
const bumpedPackage = await readJson(path.join(versionTmp, 'package.json'));
|
|
2388
2424
|
const bumpedLock = await readJson(path.join(versionTmp, 'package-lock.json'));
|
|
2389
2425
|
const bumpedChangelog = await safeReadText(path.join(versionTmp, 'CHANGELOG.md'));
|
|
2390
2426
|
if (bumpedPackage.version !== '0.1.1' || bumpedLock.version !== '0.1.1' || bumpedLock.packages[''].version !== '0.1.1') throw new Error('selftest: package lock versions not synced');
|
|
2391
|
-
if (!bumpedChangelog.includes('## [0.1.1]') || !bumpedChangelog.includes('
|
|
2427
|
+
if (!bumpedChangelog.includes('## [0.1.1]') || !bumpedChangelog.includes('explicit SKS version bump')) throw new Error('selftest: version bump did not sync changelog section');
|
|
2392
2428
|
const firstCached = await runProcess('git', ['diff', '--cached', '--name-only'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2393
2429
|
if (!firstCached.stdout.includes('package.json') || !firstCached.stdout.includes('package-lock.json') || !firstCached.stdout.includes('CHANGELOG.md')) throw new Error('selftest: version files not staged');
|
|
2394
2430
|
await runProcess('git', ['commit', '--no-verify', '-m', 'first versioned commit'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
@@ -3305,7 +3341,7 @@ async function selftest() {
|
|
|
3305
3341
|
await ensureDir(fakeTmuxDir);
|
|
3306
3342
|
const fakeTmuxLog = path.join(fakeTmuxDir, 'tmux.log');
|
|
3307
3343
|
const fakeTmuxBin = path.join(fakeTmuxDir, 'tmux');
|
|
3308
|
-
await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst{appendFileSync:a}=require('
|
|
3344
|
+
await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst{appendFileSync:a}=require('fs'),e=process.env,r=process.argv.slice(2),c=r[0];if(e.SKS_FAKE_TMUX_LOG)a(e.SKS_FAKE_TMUX_LOG,r.join(' ')+'\\n');if(c==='new-session')console.log('%1');else if(c==='split-window')console.log(e.SKS_FAKE_TMUX_SPLIT_ID||'%2');else if(c==='list-windows')console.log('@1');else if(c==='display-message')console.log(e.SKS_FAKE_TMUX_DISPLAY||'sks-existing-selftest\\t@1\\t%1');else if(c==='list-sessions')console.log(e.SKS_FAKE_TMUX_SESSIONS||'');else if(c==='list-panes'){let t=r[r.indexOf('-t')+1]||'';console.log(t[0]=='%'&&r.join(' ').includes('pane_dead')?'0\\t'+t:e.SKS_FAKE_TMUX_LIST||'')}\n`);
|
|
3309
3345
|
await fsp.chmod(fakeTmuxBin, 0o755);
|
|
3310
3346
|
const previousFakeTmuxLog = process.env.SKS_FAKE_TMUX_LOG;
|
|
3311
3347
|
const previousPath = process.env.PATH;
|
|
@@ -3346,6 +3382,91 @@ async function selftest() {
|
|
|
3346
3382
|
const cockpitOpenLog = await safeReadText(fakeTmuxLog);
|
|
3347
3383
|
if (!cockpitOpen.ok || cockpitOpen.opened_lane_count !== 2 || cockpitOpen.main_pane_id !== '%1' || cockpitOpen.relayout?.layout_name !== 'main-vertical' || !cockpitOpenLog.includes('display-message -p') || !cockpitOpenLog.includes('split-window -h -t %1') || !cockpitOpenLog.includes('set-option -pt %80 @sks_team_managed 1') || !cockpitOpenLog.includes('select-pane -t %1') || !cockpitOpenLog.includes('select-layout -t @1 main-vertical')) throw new Error('selftest: split');
|
|
3348
3384
|
await writeTextAtomic(fakeTmuxLog, '');
|
|
3385
|
+
process.env.SKS_FAKE_TMUX_SPLIT_ID = '%90';
|
|
3386
|
+
process.env.SKS_FAKE_TMUX_LIST = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%82\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%84\tscout: analysis_scout_old\tnode\t1\told-team-mission\tanalysis_scout_old\tscout`;
|
|
3387
|
+
const cockpitDedupe = await reconcileTmuxTeamCockpit({
|
|
3388
|
+
root: tmp,
|
|
3389
|
+
missionId: teamId,
|
|
3390
|
+
plan: roleTeamPlan,
|
|
3391
|
+
dashboard: { agents: { analysis_scout_1: { status: 'assigned' } } },
|
|
3392
|
+
control: { status: 'running' },
|
|
3393
|
+
tmux: { bin: fakeTmuxBin },
|
|
3394
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3395
|
+
});
|
|
3396
|
+
const cockpitDedupeLog = await safeReadText(fakeTmuxLog);
|
|
3397
|
+
if (!cockpitDedupe.ok || cockpitDedupe.closed_lane_count !== 2 || !cockpitDedupeLog.includes('kill-pane -t %82') || !cockpitDedupeLog.includes('kill-pane -t %84') || cockpitDedupeLog.includes('kill-pane -t %81')) throw new Error('selftest: tmux cockpit did not prune duplicate or stale managed panes');
|
|
3398
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3399
|
+
process.env.SKS_FAKE_TMUX_LIST = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout`;
|
|
3400
|
+
const cockpitTerminal = await reconcileTmuxTeamCockpit({
|
|
3401
|
+
root: tmp,
|
|
3402
|
+
missionId: teamId,
|
|
3403
|
+
plan: roleTeamPlan,
|
|
3404
|
+
dashboard: { agents: { analysis_scout_1: { status: 'completed' } } },
|
|
3405
|
+
control: { status: 'running' },
|
|
3406
|
+
tmux: { bin: fakeTmuxBin },
|
|
3407
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3408
|
+
});
|
|
3409
|
+
const cockpitTerminalLog = await safeReadText(fakeTmuxLog);
|
|
3410
|
+
if (!cockpitTerminal.ok || cockpitTerminal.closed_lane_count !== 1 || cockpitTerminal.opened_lane_count !== 0 || !cockpitTerminalLog.includes('kill-pane -t %81')) throw new Error('selftest: tmux cockpit did not close terminal agent pane');
|
|
3411
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3412
|
+
const staleTeamId = 'M-20260512-000000-old1';
|
|
3413
|
+
const missionDirOnlyTeamId = 'M-20260512-000000-dir1';
|
|
3414
|
+
await ensureDir(path.join(tmp, '.sneakoscope', 'missions', missionDirOnlyTeamId));
|
|
3415
|
+
await writeJsonAtomic(path.join(tmp, '.sneakoscope', 'state', 'tmux-team-sessions.json'), {
|
|
3416
|
+
schema_version: 1,
|
|
3417
|
+
missions: {
|
|
3418
|
+
[staleTeamId]: {
|
|
3419
|
+
mission_id: staleTeamId,
|
|
3420
|
+
session: `sks-team-${staleTeamId}`,
|
|
3421
|
+
root: tmp,
|
|
3422
|
+
panes: [{ pane_id: '%201', title: 'scout: analysis_scout_1' }]
|
|
3423
|
+
},
|
|
3424
|
+
[teamId]: {
|
|
3425
|
+
mission_id: teamId,
|
|
3426
|
+
session: 'sks-existing-selftest',
|
|
3427
|
+
root: tmp,
|
|
3428
|
+
panes: [{ pane_id: '%204', title: 'scout: analysis_scout_1' }]
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
});
|
|
3432
|
+
process.env.SKS_FAKE_TMUX_LIST = [
|
|
3433
|
+
'sks-existing-selftest\t@1\t%1\tCodex CLI\tnode\t\t\t\t',
|
|
3434
|
+
`sks-team-${staleTeamId}\t@70\t%201\tscout: analysis_scout_1\tnode\t\t\t\t`,
|
|
3435
|
+
`sks-existing-selftest\t@1\t%202\treview: stale_review\tnode\t1\t${staleTeamId}\tstale_review\treview`,
|
|
3436
|
+
`sks-team-${missionDirOnlyTeamId}\t@71\t%205\treview: reviewer_1\tnode\t\t\t\t`,
|
|
3437
|
+
'unrelated-session\t@9\t%203\tscout: analysis_scout_1\tnode\t\t\t\t',
|
|
3438
|
+
`sks-existing-selftest\t@1\t%204\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout`
|
|
3439
|
+
].join('\n');
|
|
3440
|
+
const cockpitSweep = await sweepTmuxTeamSurfaces({
|
|
3441
|
+
root: tmp,
|
|
3442
|
+
keepMissionId: teamId,
|
|
3443
|
+
tmux: { bin: fakeTmuxBin },
|
|
3444
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3445
|
+
});
|
|
3446
|
+
const cockpitSweepLog = await safeReadText(fakeTmuxLog);
|
|
3447
|
+
if (!cockpitSweep.ok || cockpitSweep.closed_lane_count !== 3 || !cockpitSweepLog.includes('kill-pane -t %201') || !cockpitSweepLog.includes('kill-pane -t %202') || !cockpitSweepLog.includes('kill-pane -t %205') || cockpitSweepLog.includes('kill-pane -t %203') || cockpitSweepLog.includes('kill-pane -t %204') || cockpitSweepLog.includes('kill-pane -t %1')) throw new Error('selftest: tmux sweep did not close only stale recorded Team panes');
|
|
3448
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3449
|
+
const codexLbSuffix = defaultTmuxSessionName(tmp);
|
|
3450
|
+
const codexLbKeepSession = `sks-codex-lb-keep-${codexLbSuffix}`;
|
|
3451
|
+
const codexLbCurrentSession = `sks-codex-lb-current-${codexLbSuffix}`;
|
|
3452
|
+
process.env.SKS_FAKE_TMUX_DISPLAY = `${codexLbCurrentSession}\t@1\t%1`;
|
|
3453
|
+
process.env.SKS_FAKE_TMUX_SESSIONS = [
|
|
3454
|
+
`sks-codex-lb-old-${codexLbSuffix}\t0\t100\t100`,
|
|
3455
|
+
`sks-codex-lb-attached-${codexLbSuffix}\t1\t101\t101`,
|
|
3456
|
+
`${codexLbKeepSession}\t0\t102\t102`,
|
|
3457
|
+
`${codexLbCurrentSession}\t0\t103\t103`,
|
|
3458
|
+
'sks-codex-lb-other-sks-other-00000000\t0\t104\t104'
|
|
3459
|
+
].join('\n');
|
|
3460
|
+
const codexLbSweep = await sweepCodexLbTmuxSessions({
|
|
3461
|
+
root: tmp,
|
|
3462
|
+
keepSession: codexLbKeepSession,
|
|
3463
|
+
tmux: { bin: fakeTmuxBin },
|
|
3464
|
+
env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
|
|
3465
|
+
});
|
|
3466
|
+
const codexLbSweepLog = await safeReadText(fakeTmuxLog);
|
|
3467
|
+
if (!codexLbSweep.ok || codexLbSweep.closed_session_count !== 1 || !codexLbSweepLog.includes(`kill-session -t sks-codex-lb-old-${codexLbSuffix}`) || codexLbSweepLog.includes(`kill-session -t sks-codex-lb-attached-${codexLbSuffix}`) || codexLbSweepLog.includes(`kill-session -t ${codexLbKeepSession}`) || codexLbSweepLog.includes(`kill-session -t ${codexLbCurrentSession}`) || codexLbSweepLog.includes('kill-session -t sks-codex-lb-other')) throw new Error('selftest: codex-lb tmux sweep did not close only stale detached sessions for this repo');
|
|
3468
|
+
await writeTextAtomic(fakeTmuxLog, '');
|
|
3469
|
+
process.env.SKS_FAKE_TMUX_DISPLAY = 'sks-existing-selftest\t@1\t%1';
|
|
3349
3470
|
const fakePanes = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%82\tscout: analysis_scout_2\tnode\t1\t${teamId}\tanalysis_scout_2\tscout\n%83\tuser pane\tzsh\t\t\t\t`;
|
|
3350
3471
|
process.env.SKS_FAKE_TMUX_LIST = fakePanes;
|
|
3351
3472
|
const cockpitClose = await reconcileTmuxTeamCockpit({
|
|
@@ -3362,6 +3483,7 @@ async function selftest() {
|
|
|
3362
3483
|
if (!cockpitClose.ok || cockpitClose.closed_lane_count !== 2 || !cockpitCloseLog.includes('kill-pane -t %81') || !cockpitCloseLog.includes('kill-pane -t %82') || cockpitCloseLog.includes('kill-pane -t %83')) throw new Error('selftest: cleanup');
|
|
3363
3484
|
delete process.env.SKS_FAKE_TMUX_DISPLAY;
|
|
3364
3485
|
delete process.env.SKS_FAKE_TMUX_LIST;
|
|
3486
|
+
delete process.env.SKS_FAKE_TMUX_SESSIONS;
|
|
3365
3487
|
delete process.env.SKS_FAKE_TMUX_SPLIT_ID;
|
|
3366
3488
|
await writeTextAtomic(fakeTmuxLog, '');
|
|
3367
3489
|
const madCockpit = await launchMadTmuxUi(['--workspace', 'sks-mad-selftest-ui', '--no-attach'], { root: tmp, tmux: { ok: true, bin: fakeTmuxBin, version: '3.4' }, codex: { bin: process.execPath }, app: { ok: true, guidance: [] }, missionId: 'M-MAD-SELFTEST' });
|
|
@@ -3460,9 +3582,26 @@ async function selftest() {
|
|
|
3460
3582
|
if (!teamLive.includes('selftest mapped options')) throw new Error('selftest: team live transcript missing event');
|
|
3461
3583
|
if (!teamLive.includes('Context tracking SSOT: TriWiki')) throw new Error('selftest: team live transcript missing TriWiki context tracking');
|
|
3462
3584
|
if (!(await readTeamTranscriptTail(teamDir, 1)).join('\n').includes('selftest mapped options')) throw new Error('selftest: team transcript tail missing event');
|
|
3463
|
-
const teamLane = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4 });
|
|
3585
|
+
const teamLane = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4, color: false });
|
|
3464
3586
|
if (!teamLane.includes('selftest mapped repo slice')) throw new Error('selftest: team agent lane missing event context');
|
|
3465
|
-
|
|
3587
|
+
const missingChatLaneParts = [
|
|
3588
|
+
['codex chat heading', '## Codex Chat'],
|
|
3589
|
+
['lane speaker', 'me (analysis_scout_1)'],
|
|
3590
|
+
['status role metadata', '[status/scout]'],
|
|
3591
|
+
['agent event body', 'selftest mapped repo slice']
|
|
3592
|
+
].filter(([, needle]) => !teamLane.includes(needle)).map(([label]) => label);
|
|
3593
|
+
if (missingChatLaneParts.length || teamLane.includes('## Global Tail')) {
|
|
3594
|
+
const reason = [
|
|
3595
|
+
missingChatLaneParts.length ? `missing ${missingChatLaneParts.join(', ')}` : null,
|
|
3596
|
+
teamLane.includes('## Global Tail') ? 'unexpected global tail' : null
|
|
3597
|
+
].filter(Boolean).join('; ');
|
|
3598
|
+
throw new Error(`selftest: chat lane (${reason})\n${teamLane.slice(0, 1600)}`);
|
|
3599
|
+
}
|
|
3600
|
+
if (!teamLane.includes('╭─') || !teamLane.includes('│ selftest mapped repo slice') || !teamLane.includes('╰─')) {
|
|
3601
|
+
throw new Error(`selftest: team chat lane did not render framed chat blocks\n${teamLane.slice(0, 1600)}`);
|
|
3602
|
+
}
|
|
3603
|
+
const teamLaneColor = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4, color: true });
|
|
3604
|
+
if (!/\x1b\[[0-9;]+m/.test(teamLaneColor) || !teamLaneColor.includes('Lane color:')) throw new Error('selftest: team chat lane did not render ANSI color metadata/output');
|
|
3466
3605
|
const teamLaneCli = await runProcess(process.execPath, [hookBin, 'team', 'lane', teamId, '--agent', 'analysis_scout_1', '--lines', '4'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
3467
3606
|
if (teamLaneCli.code !== 0 || !String(teamLaneCli.stdout || '').includes('SKS Team Agent Lane') || !String(teamLaneCli.stdout || '').includes('analysis_scout_1')) throw new Error('selftest: sks team lane CLI did not render an agent lane');
|
|
3468
3607
|
await writeTextAtomic(path.join(teamDir, 'team-analysis.md'), '- claim: analysis scout mapped route registry | source: src/core/routes.mjs | risk: high | confidence: supported\n');
|