sneakoscope 0.7.38 → 0.7.40
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 +48 -1
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +82 -0
- package/src/cli/main.mjs +24 -16
- package/src/core/fsx.mjs +1 -1
- package/src/core/init.mjs +113 -27
- package/src/core/tmux-ui.mjs +14 -1
- package/src/core/version-manager.mjs +51 -2
package/README.md
CHANGED
|
@@ -166,10 +166,31 @@ sks tmux check
|
|
|
166
166
|
sks tmux status --once
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
Bare `sks` creates or reuses the default named tmux session for Codex CLI and attaches to it in an interactive terminal. Use `sks tmux open` when you need explicit `--workspace` / `--session` flags, `sks tmux check` for readiness without launching, and `sks help` for CLI help. Use `--no-attach` or `SKS_TMUX_NO_AUTO_ATTACH=1` when you only want SKS to create/reuse the session and print the manual attach command.
|
|
169
|
+
Bare `sks` creates or reuses the default named tmux session for Codex CLI and attaches to it in an interactive terminal. By default it launches Codex in the SKS fast-high runtime (`--model gpt-5.5 -c model_reasoning_effort="high"`). Override with `SKS_CODEX_MODEL`, `SKS_CODEX_REASONING`, or disable the default with `SKS_CODEX_FAST_HIGH=0`. Use `sks tmux open` when you need explicit `--workspace` / `--session` flags, `sks tmux check` for readiness without launching, and `sks help` for CLI help. Use `--no-attach` or `SKS_TMUX_NO_AUTO_ATTACH=1` when you only want SKS to create/reuse the session and print the manual attach command.
|
|
170
170
|
|
|
171
171
|
Before opening tmux, SKS checks the installed Codex CLI against npm `@openai/codex@latest`. If a newer version exists, it asks `Y/n`; answering `y` updates automatically with `npm i -g @openai/codex@latest` and then opens tmux with the updated Codex CLI.
|
|
172
172
|
|
|
173
|
+
If you use [codex-lb](https://github.com/Soju06/codex-lb), start it first, create an API key in its dashboard, then add this provider to `~/.codex/config.toml`:
|
|
174
|
+
|
|
175
|
+
```toml
|
|
176
|
+
model_provider = "codex-lb"
|
|
177
|
+
|
|
178
|
+
[model_providers.codex-lb]
|
|
179
|
+
name = "OpenAI"
|
|
180
|
+
base_url = "http://127.0.0.1:2455/backend-api/codex"
|
|
181
|
+
wire_api = "responses"
|
|
182
|
+
env_key = "CODEX_LB_API_KEY"
|
|
183
|
+
supports_websockets = true
|
|
184
|
+
requires_openai_auth = true
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Then run:
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
export CODEX_LB_API_KEY="sk-clb-..."
|
|
191
|
+
sks
|
|
192
|
+
```
|
|
193
|
+
|
|
173
194
|
### MAD tmux Launch
|
|
174
195
|
|
|
175
196
|
```sh
|
|
@@ -385,12 +406,38 @@ Use these inside Codex App or another agent prompt. They are prompt commands, no
|
|
|
385
406
|
|
|
386
407
|
### First Install Checklist
|
|
387
408
|
|
|
409
|
+
1. Install SKS.
|
|
410
|
+
|
|
388
411
|
```sh
|
|
389
412
|
npm i -g sneakoscope
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
2. Bootstrap and check dependencies.
|
|
416
|
+
|
|
417
|
+
```sh
|
|
390
418
|
sks bootstrap
|
|
391
419
|
sks deps check
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
On macOS, missing tmux installs and Homebrew-managed tmux upgrades ask `Y/n` before running `brew install tmux` or `brew upgrade tmux`. If PATH resolves an npm-managed `tmux`, SKS prompts for `npm i -g tmux@latest` instead of using Homebrew. Unknown non-Homebrew `tmux` paths are reported as conflicts so the user can remove, upgrade with the owning package manager, or reorder PATH first.
|
|
423
|
+
|
|
424
|
+
3. Confirm Codex App command surfaces.
|
|
425
|
+
|
|
426
|
+
```sh
|
|
392
427
|
sks codex-app check
|
|
393
428
|
sks dollar-commands
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
4. Optional codex-lb key setup for CLI `sks` runs.
|
|
432
|
+
|
|
433
|
+
```sh
|
|
434
|
+
export CODEX_LB_API_KEY="sk-clb-..."
|
|
435
|
+
sks
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
5. Run a local smoke test.
|
|
439
|
+
|
|
440
|
+
```sh
|
|
394
441
|
sks selftest --mock
|
|
395
442
|
```
|
|
396
443
|
|
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.40",
|
|
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",
|
|
@@ -222,6 +222,7 @@ async function ensureGlobalGetdesignSkillDuringInstall() {
|
|
|
222
222
|
export async function ensureRelatedCliTools(args = []) {
|
|
223
223
|
const skip = args.includes('--skip-cli-tools') || process.env.SKS_SKIP_CLI_TOOLS === '1';
|
|
224
224
|
const codex = await ensureCodexCliTool({ skip });
|
|
225
|
+
const tmuxRepair = skip ? { status: 'skipped', reason: 'SKS_SKIP_CLI_TOOLS=1 or --skip-cli-tools' } : await ensureTmuxCliTool(args);
|
|
225
226
|
const tmux = await tmuxReadiness().catch((err) => ({ ok: false, version: null, error: err.message }));
|
|
226
227
|
return {
|
|
227
228
|
codex,
|
|
@@ -231,6 +232,7 @@ export async function ensureRelatedCliTools(args = []) {
|
|
|
231
232
|
version: tmux.version || null,
|
|
232
233
|
min_version: tmux.min_version || '3.0',
|
|
233
234
|
current_session: Boolean(tmux.current_session),
|
|
235
|
+
repair: tmuxRepair,
|
|
234
236
|
install_hint: tmux.ok ? null : platformTmuxInstallHint(),
|
|
235
237
|
error: tmux.error || null
|
|
236
238
|
}
|
|
@@ -259,6 +261,86 @@ export async function ensureCodexCliTool({ skip = false } = {}) {
|
|
|
259
261
|
};
|
|
260
262
|
}
|
|
261
263
|
|
|
264
|
+
export async function ensureTmuxCliTool(args = [], opts = {}) {
|
|
265
|
+
const before = await tmuxReadiness().catch((err) => ({ ok: false, error: err.message }));
|
|
266
|
+
if (before.ok) return { target: 'tmux', status: 'present', bin: before.bin || null, version: before.version || null };
|
|
267
|
+
const command = process.platform === 'darwin' ? 'brew install tmux' : platformTmuxInstallHint();
|
|
268
|
+
if (process.platform !== 'darwin') return { target: 'tmux', status: 'manual_required', command, error: before.error || 'tmux not found' };
|
|
269
|
+
const brew = await which('brew').catch(() => null);
|
|
270
|
+
if (!brew) return { target: 'tmux', status: 'manual_required', command: 'Install Homebrew, then run: brew install tmux', error: before.error || 'tmux not found' };
|
|
271
|
+
const origin = await tmuxInstallOrigin(before.bin, brew);
|
|
272
|
+
if (before.bin && origin.manager === 'npm') {
|
|
273
|
+
const repairCommand = 'npm i -g tmux@latest';
|
|
274
|
+
if (args.includes('--dry-run') || opts.dryRun) return { target: 'tmux', status: 'dry_run', manager: 'npm', command: repairCommand, error: before.error || null };
|
|
275
|
+
const npmBin = await which('npm').catch(() => null);
|
|
276
|
+
if (!npmBin) return { target: 'tmux', status: 'manual_required', manager: 'npm', command: repairCommand, error: 'npm not found on PATH' };
|
|
277
|
+
const question = `npm-managed tmux ${before.version || 'unknown'} is not ready. Upgrade with ${repairCommand}?`;
|
|
278
|
+
if (!await confirmInstallYesDefault(question, args)) return { target: 'tmux', status: 'needs_approval', manager: 'npm', command: repairCommand, error: before.error || null };
|
|
279
|
+
const install = await runProcess(npmBin, ['i', '-g', 'tmux@latest'], { timeoutMs: 180000, maxOutputBytes: 128 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
280
|
+
if (install.code !== 0) return { target: 'tmux', status: 'failed', manager: 'npm', command: repairCommand, error: `${install.stderr || install.stdout || repairCommand + ' failed'}`.trim() };
|
|
281
|
+
const after = await tmuxReadiness().catch((err) => ({ ok: false, error: err.message }));
|
|
282
|
+
if (!after.ok) return { target: 'tmux', status: 'installed_not_ready', manager: 'npm', command: repairCommand, error: after.error || 'tmux upgraded with npm but is still not ready' };
|
|
283
|
+
return { target: 'tmux', status: 'upgraded', manager: 'npm', command: repairCommand, bin: after.bin || null, version: after.version || null };
|
|
284
|
+
}
|
|
285
|
+
if (before.bin && origin.manager !== 'homebrew') {
|
|
286
|
+
return {
|
|
287
|
+
target: 'tmux',
|
|
288
|
+
status: 'conflicting_tmux',
|
|
289
|
+
bin: before.bin,
|
|
290
|
+
version: before.version || null,
|
|
291
|
+
manager: origin.manager,
|
|
292
|
+
command,
|
|
293
|
+
error: `${before.error || 'tmux is not ready'}; PATH resolves an unknown non-Homebrew tmux (${origin.reason}). Remove, upgrade with its owning package manager, or reorder PATH first, then run: ${command}`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const repairCommand = before.bin ? 'brew upgrade tmux' : command;
|
|
297
|
+
if (args.includes('--dry-run') || opts.dryRun) return { target: 'tmux', status: 'dry_run', command: repairCommand, error: before.error || null };
|
|
298
|
+
const question = before.bin
|
|
299
|
+
? `Homebrew tmux ${before.version || 'unknown'} is too old. Upgrade to latest tmux with ${repairCommand}?`
|
|
300
|
+
: `tmux is missing. Install latest tmux with ${repairCommand}?`;
|
|
301
|
+
if (!await confirmInstallYesDefault(question, args)) return { target: 'tmux', status: 'needs_approval', command: repairCommand, error: before.error || null };
|
|
302
|
+
const brewArgs = before.bin ? ['upgrade', 'tmux'] : ['install', 'tmux'];
|
|
303
|
+
const install = await runProcess(brew, brewArgs, { timeoutMs: 180000, maxOutputBytes: 128 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
304
|
+
if (install.code !== 0) return { target: 'tmux', status: 'failed', command: repairCommand, error: `${install.stderr || install.stdout || repairCommand + ' failed'}`.trim() };
|
|
305
|
+
const after = await tmuxReadiness().catch((err) => ({ ok: false, error: err.message }));
|
|
306
|
+
if (!after.ok) return { target: 'tmux', status: 'installed_not_ready', command: repairCommand, error: after.error || 'tmux installed but not ready' };
|
|
307
|
+
return { target: 'tmux', status: before.bin ? 'upgraded' : 'installed', command: repairCommand, bin: after.bin || null, version: after.version || null };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function confirmInstallYesDefault(question, args = []) {
|
|
311
|
+
if (shouldAutoApproveInstall(args)) return true;
|
|
312
|
+
if (!canAskYesNo()) return false;
|
|
313
|
+
const answer = (await askPostinstallQuestion(`${question} [Y/n] `)).trim();
|
|
314
|
+
return answer === '' || /^(y|yes|예|네|응)$/i.test(answer);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function tmuxInstallOrigin(bin, brewBin) {
|
|
318
|
+
if (!bin) return { manager: 'missing', reason: 'tmux not found on PATH' };
|
|
319
|
+
const resolved = await fsp.realpath(bin).catch(() => path.resolve(bin));
|
|
320
|
+
if (brewBin) {
|
|
321
|
+
const brewPrefix = await runProcess(brewBin, ['--prefix'], { timeoutMs: 5000, maxOutputBytes: 4096 }).catch(() => null);
|
|
322
|
+
const prefix = brewPrefix?.code === 0 ? brewPrefix.stdout.trim().split(/\r?\n/).pop() : '';
|
|
323
|
+
const brewTmux = await runProcess(brewBin, ['list', '--versions', 'tmux'], { timeoutMs: 5000, maxOutputBytes: 4096 }).catch(() => null);
|
|
324
|
+
if (prefix && resolved.startsWith(path.resolve(prefix) + path.sep) && brewTmux?.code === 0) {
|
|
325
|
+
return { manager: 'homebrew', reason: `${resolved} under ${prefix}` };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const npmBin = await which('npm').catch(() => null);
|
|
329
|
+
if (npmBin) {
|
|
330
|
+
const npmPrefix = await runProcess(npmBin, ['prefix', '-g'], { timeoutMs: 5000, maxOutputBytes: 4096 }).catch(() => null);
|
|
331
|
+
const prefix = npmPrefix?.code === 0 ? npmPrefix.stdout.trim().split(/\r?\n/).pop() : '';
|
|
332
|
+
const npmBinDir = prefix ? (process.platform === 'win32' ? prefix : path.join(prefix, 'bin')) : '';
|
|
333
|
+
const npmRoot = prefix ? path.join(prefix, 'lib', 'node_modules') : '';
|
|
334
|
+
if ((npmBinDir && path.resolve(bin).startsWith(path.resolve(npmBinDir) + path.sep)) || (npmRoot && resolved.startsWith(path.resolve(npmRoot) + path.sep))) {
|
|
335
|
+
return { manager: 'npm', reason: `${bin} resolves through npm global prefix ${prefix}` };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (/\/node_modules\/(?:\.bin\/)?tmux(?:$|\/)/.test(resolved.split(path.sep).join('/'))) {
|
|
339
|
+
return { manager: 'npm', reason: `${resolved} is inside node_modules` };
|
|
340
|
+
}
|
|
341
|
+
return { manager: 'unknown', reason: `${bin} resolves to ${resolved}` };
|
|
342
|
+
}
|
|
343
|
+
|
|
262
344
|
export async function maybePromptCodexUpdateForLaunch(args = [], opts = {}) {
|
|
263
345
|
if (hasFlag(args, '--json') || hasFlag(args, '--skip-cli-tools') || hasFlag(args, '--skip-codex-update') || process.env.SKS_SKIP_CODEX_UPDATE === '1') return { status: 'skipped' };
|
|
264
346
|
const latest = await npmPackageVersion('@openai/codex');
|
package/src/cli/main.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { classifySql, classifyCommand, checkDbOperation, handleMadSksUserConfirm
|
|
|
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 { installVersionGitHook, runVersionPreCommit, versioningStatus } from '../core/version-manager.mjs';
|
|
21
|
+
import { bumpProjectVersion, installVersionGitHook, 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';
|
|
@@ -61,7 +61,7 @@ import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs'
|
|
|
61
61
|
import { buildTmuxLaunchPlan, buildTmuxOpenArgs, createTmuxSession, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
|
|
62
62
|
import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
|
|
63
63
|
import { context7Command } from './context7-command.mjs';
|
|
64
|
-
import { askPostinstallQuestion, checkContext7, checkRequiredSkills, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, globalCodexSkillsRoot, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, shouldAutoApproveInstall } from './install-helpers.mjs';
|
|
64
|
+
import { askPostinstallQuestion, checkContext7, checkRequiredSkills, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, shouldAutoApproveInstall } from './install-helpers.mjs';
|
|
65
65
|
import { buildTeamPlan, codeStructureCommand, dbCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, harnessCommand, hproofCommand, memoryCommand, migrateWikiContextPack, parseTeamCreateArgs, perfCommand, profileCommand, projectWikiClaims, proofFieldCommand, qaLoopCommand, quickstartCommand, researchCommand, skillDreamCommand, statsCommand, team, teamWorkflowMarkdown, validateArtifactsCommand, wikiCommand, wikiVoxelRowCount, writeWikiContextPack } from './maintenance-commands.mjs';
|
|
66
66
|
import { openClawCommand } from './openclaw-command.mjs';
|
|
67
67
|
|
|
@@ -819,7 +819,7 @@ async function versioning(sub = 'status', args = []) {
|
|
|
819
819
|
return;
|
|
820
820
|
}
|
|
821
821
|
if (action === 'bump') {
|
|
822
|
-
const res = await
|
|
822
|
+
const res = await bumpProjectVersion(root, { force: true });
|
|
823
823
|
if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
|
|
824
824
|
if (!res.ok) {
|
|
825
825
|
console.error(`Version bump failed: ${res.reason || 'unknown'}`);
|
|
@@ -839,7 +839,7 @@ async function versioning(sub = 'status', args = []) {
|
|
|
839
839
|
return;
|
|
840
840
|
}
|
|
841
841
|
if (res.skipped) return;
|
|
842
|
-
console.log(res.changed ? `SKS versioning: ${res.
|
|
842
|
+
console.log(res.changed ? `SKS versioning synced: ${res.version}` : `SKS versioning: ${res.version} verified`);
|
|
843
843
|
return;
|
|
844
844
|
}
|
|
845
845
|
console.error('Usage: sks versioning status|bump|pre-commit [--json]');
|
|
@@ -1106,11 +1106,7 @@ async function installContext7Dependency(root) {
|
|
|
1106
1106
|
}
|
|
1107
1107
|
|
|
1108
1108
|
async function installTmuxDependency(args = []) {
|
|
1109
|
-
|
|
1110
|
-
if (before.ok) return { target: 'tmux', status: 'present', version: before.version || null, app: before.app || null, cli: before.cli || null };
|
|
1111
|
-
const command = process.platform === 'darwin' ? 'brew install tmux' : platformTmuxInstallHint();
|
|
1112
|
-
if (flag(args, '--dry-run')) return { target: 'tmux', status: 'dry_run', command };
|
|
1113
|
-
return { target: 'tmux', status: 'manual_required', command, error: before.error || 'tmux not found' };
|
|
1109
|
+
return ensureTmuxCliTool(args, { dryRun: flag(args, '--dry-run') });
|
|
1114
1110
|
}
|
|
1115
1111
|
|
|
1116
1112
|
async function confirmInstall(question, args = []) {
|
|
@@ -1237,11 +1233,11 @@ function usage(args = []) {
|
|
|
1237
1233
|
const topic = String(args[0] || 'overview').toLowerCase();
|
|
1238
1234
|
const blocks = {
|
|
1239
1235
|
overview: ['ㅅㅋㅅ 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}`],
|
|
1240
|
-
install: ['Install', '', ' npm i -g sneakoscope', ' sks
|
|
1236
|
+
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:', ' # Add the codex-lb provider to ~/.codex/config.toml, then:', ' export CODEX_LB_API_KEY="sk-clb-..."', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
|
|
1241
1237
|
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.'],
|
|
1242
1238
|
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.'],
|
|
1243
|
-
deps: ['Dependencies', '', ' sks deps check [--json]', ' sks deps install [tmux|codex|context7|all] [--yes]', '', 'tmux on macOS uses Homebrew
|
|
1244
|
-
tmux: ['tmux', '', ' sks', ' sks tmux open', ' sks tmux check', ' sks tmux status --once', ' sks deps install tmux', '', 'Running bare `sks` opens or reuses the default tmux Codex CLI session. Before launch, SKS checks npm @openai/codex@latest and prompts Y/n when the installed Codex CLI is missing or outdated. Use `sks tmux open` when you need explicit session/workspace flags, and `sks help` for CLI help.'],
|
|
1239
|
+
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.'],
|
|
1240
|
+
tmux: ['tmux', '', ' sks', ' sks tmux open', ' sks tmux check', ' sks tmux status --once', ' sks deps install tmux', '', 'Running bare `sks` opens or reuses the default tmux Codex CLI session in fast-high mode: --model gpt-5.5 -c model_reasoning_effort="high". Override with SKS_CODEX_MODEL or SKS_CODEX_REASONING. Before launch, SKS checks npm @openai/codex@latest and prompts Y/n when the installed Codex CLI is missing or outdated. Use `sks tmux open` when you need explicit session/workspace flags, and `sks help` for CLI help.'],
|
|
1245
1241
|
openclaw: ['OpenClaw', '', ' sks openclaw install', ' sks openclaw path', ' sks openclaw print SKILL.md', '', 'Installs an OpenClaw skill package under ~/.openclaw/skills/sneakoscope-codex so OpenClaw agents can attach skills: [sneakoscope-codex] with the shell tool and call local SKS commands from a project root.'],
|
|
1246
1242
|
team: ['Team', '', ' sks team "task" executor:5 reviewer:2 user:1', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', ' sks team message latest --from analysis_scout_1 --to executor_1 --message "handoff note"', ' sks team cleanup-tmux latest', '', '$Team runs questions -> contract -> scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
|
|
1247
1243
|
'qa-loop': ['QA-LOOP', '', ' sks qa-loop prepare "QA this app"', ' sks qa-loop answer <MISSION_ID> answers.json', ' sks qa-loop run <MISSION_ID> --max-cycles 8', '', 'Report: YYYY-MM-DD-v<version>-qa-report.md'],
|
|
@@ -1937,6 +1933,8 @@ async function selftest() {
|
|
|
1937
1933
|
if (!tmuxSyntax.ok || !tmuxSyntax.command.includes('tmux attach-session -t sks-mad-selftest')) throw new Error('selftest failed: MAD tmux attach plan is not stable by session name');
|
|
1938
1934
|
const tmuxOpenArgs = buildTmuxOpenArgs(workspacePlan);
|
|
1939
1935
|
if (tmuxOpenArgs.join(' ') !== 'attach-session -t sks-mad-selftest') throw new Error('selftest failed: MAD tmux attach args are not stable by session name');
|
|
1936
|
+
const defaultFastHighPlan = await buildTmuxLaunchPlan({ root: tmp, tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
|
|
1937
|
+
if (defaultFastHighPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c model_reasoning_effort="high"') throw new Error('selftest failed: default sks tmux launch is not fast-high');
|
|
1940
1938
|
if (!shouldAutoAttachTmux(['--mad'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux launch does not auto-attach in an interactive terminal');
|
|
1941
1939
|
if (shouldAutoAttachTmux(['--mad', '--json'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux json mode should not auto-attach');
|
|
1942
1940
|
if (shouldAutoAttachTmux(['--mad', '--no-attach'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux --no-attach should remain print-only');
|
|
@@ -2037,9 +2035,12 @@ async function selftest() {
|
|
|
2037
2035
|
const versionHookText = await safeReadText(versionStatus.hook_path);
|
|
2038
2036
|
if (!versionHookText.includes('versioning pre-commit')) throw new Error('selftest failed: versioning hook command missing');
|
|
2039
2037
|
if (versionHookText.indexOf('versioning pre-commit') > versionHookText.indexOf('exit 0')) throw new Error('selftest failed: versioning hook was appended after an early exit');
|
|
2038
|
+
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');
|
|
2040
2039
|
await writeTextAtomic(path.join(versionTmp, 'README.md'), 'version selftest\n');
|
|
2041
|
-
await runProcess('git', ['add', 'README.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2042
|
-
const
|
|
2040
|
+
await runProcess('git', ['add', 'README.md', 'CHANGELOG.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2041
|
+
const preCommitVerify = await runVersionPreCommit(versionTmp);
|
|
2042
|
+
if (!preCommitVerify.ok || preCommitVerify.version !== '0.1.0' || preCommitVerify.changed) throw new Error('selftest failed: pre-commit should verify current version without bumping');
|
|
2043
|
+
const firstVersionBump = await bumpProjectVersion(versionTmp);
|
|
2043
2044
|
if (!firstVersionBump.ok || firstVersionBump.version !== '0.1.1' || !firstVersionBump.changed) throw new Error('selftest failed: first version bump did not advance patch version');
|
|
2044
2045
|
const bumpedPackage = await readJson(path.join(versionTmp, 'package.json'));
|
|
2045
2046
|
const bumpedLock = await readJson(path.join(versionTmp, 'package-lock.json'));
|
|
@@ -2052,7 +2053,7 @@ async function selftest() {
|
|
|
2052
2053
|
await writeJsonAtomic(versionStatus.state_path, { schema_version: 1, last_version: '0.1.5', updated_at: nowIso(), pid: process.pid, changed: true });
|
|
2053
2054
|
await writeTextAtomic(path.join(versionTmp, 'CHANGELOG.md'), 'collision selftest\n');
|
|
2054
2055
|
await runProcess('git', ['add', 'CHANGELOG.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2055
|
-
const collisionBump = await
|
|
2056
|
+
const collisionBump = await bumpProjectVersion(versionTmp);
|
|
2056
2057
|
if (!collisionBump.ok || collisionBump.version !== '0.1.6') throw new Error('selftest failed: version collision state did not bump above last seen version');
|
|
2057
2058
|
const localOnlyTmp = tmpdir();
|
|
2058
2059
|
await ensureDir(path.join(localOnlyTmp, '.git'));
|
|
@@ -2547,9 +2548,16 @@ async function selftest() {
|
|
|
2547
2548
|
const codexConfigText = await safeReadText(path.join(tmp, '.codex', 'config.toml'));
|
|
2548
2549
|
if (!codexConfigText.includes('multi_agent = true')) throw new Error('selftest failed: multi_agent not enabled');
|
|
2549
2550
|
if (!hasContext7ConfigText(codexConfigText)) throw new Error('selftest failed: Context7 MCP not configured');
|
|
2550
|
-
if (!codexConfigText.includes('[profiles.sks-task-low]') || !codexConfigText.includes('[profiles.sks-task-medium]') || !codexConfigText.includes('[profiles.sks-logic-high]') || !codexConfigText.includes('[profiles.sks-research-xhigh]') || !codexConfigText.includes('[profiles.sks-mad-high]')) throw new Error('selftest failed: GPT-5.5 reasoning profiles not configured');
|
|
2551
|
+
if (!codexConfigText.includes('[profiles.sks-task-low]') || !codexConfigText.includes('[profiles.sks-task-medium]') || !codexConfigText.includes('[profiles.sks-logic-high]') || !codexConfigText.includes('[profiles.sks-fast-high]') || !codexConfigText.includes('[profiles.sks-research-xhigh]') || !codexConfigText.includes('[profiles.sks-mad-high]')) throw new Error('selftest failed: GPT-5.5 reasoning profiles not configured');
|
|
2551
2552
|
if (!codexConfigText.includes('[agents.analysis_scout]')) throw new Error('selftest failed: analysis_scout agent not configured');
|
|
2552
2553
|
if (!codexConfigText.includes('[agents.team_consensus]')) throw new Error('selftest failed: team_consensus agent not configured');
|
|
2554
|
+
const preservedConfigTmp = tmpdir();
|
|
2555
|
+
await ensureDir(path.join(preservedConfigTmp, '.codex'));
|
|
2556
|
+
await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), '[features]\nfast_mode_ui = true\n\n[user.fast_mode]\nvisible = true\n');
|
|
2557
|
+
await initProject(preservedConfigTmp, {});
|
|
2558
|
+
const preservedConfig = await safeReadText(path.join(preservedConfigTmp, '.codex', 'config.toml'));
|
|
2559
|
+
if (!preservedConfig.includes('fast_mode_ui = true') || !preservedConfig.includes('[user.fast_mode]') || !preservedConfig.includes('visible = true')) throw new Error('selftest failed: Codex config merge dropped user Fast mode settings');
|
|
2560
|
+
if (!preservedConfig.includes('codex_hooks = true') || !preservedConfig.includes('[profiles.sks-fast-high]')) throw new Error('selftest failed: Codex config merge did not add SKS managed settings');
|
|
2553
2561
|
const autoReviewHome = path.join(tmp, 'auto-review-home');
|
|
2554
2562
|
const autoReviewEnv = { HOME: autoReviewHome };
|
|
2555
2563
|
const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
|
package/src/core/fsx.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
|
|
8
|
-
export const PACKAGE_VERSION = '0.7.
|
|
8
|
+
export const PACKAGE_VERSION = '0.7.40';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
package/src/core/init.mjs
CHANGED
|
@@ -415,15 +415,118 @@ export async function initProject(root, opts = {}) {
|
|
|
415
415
|
};
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
418
|
+
function installPolicy(scope, commandPrefix) {
|
|
419
|
+
return {
|
|
420
|
+
scope,
|
|
421
|
+
default_scope: 'global',
|
|
422
|
+
hook_command_prefix: commandPrefix,
|
|
423
|
+
global_install: 'npm i -g sneakoscope',
|
|
424
|
+
project_install: 'npm i -D sneakoscope && npx sks setup --install-scope project'
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function mergeManagedCodexConfigToml(existingContent = '') {
|
|
429
|
+
let next = String(existingContent || '').trimEnd();
|
|
430
|
+
next = upsertTomlTableKey(next, 'features', 'codex_hooks = true');
|
|
431
|
+
next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
|
|
432
|
+
next = upsertTomlTableKey(next, 'agents', 'max_threads = 6');
|
|
433
|
+
next = upsertTomlTableKey(next, 'agents', 'max_depth = 1');
|
|
434
|
+
for (const block of managedCodexConfigBlocks()) {
|
|
435
|
+
next = upsertTomlTable(next, block.table, block.text);
|
|
436
|
+
}
|
|
437
|
+
return `${next.trim()}\n`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function managedCodexConfigBlocks() {
|
|
441
|
+
return [
|
|
442
|
+
{ table: 'mcp_servers.context7', text: context7ConfigToml().trim() },
|
|
443
|
+
{ table: 'agents.analysis_scout', text: agentConfigBlock('analysis_scout', 'Read-only SKS scout.', './agents/analysis-scout.toml', ['Scout', 'Mapper']) },
|
|
444
|
+
{ table: 'agents.team_consensus', text: agentConfigBlock('team_consensus', 'SKS planning/debate agent.', './agents/team-consensus.toml', ['Consensus', 'Atlas']) },
|
|
445
|
+
{ table: 'agents.implementation_worker', text: agentConfigBlock('implementation_worker', 'SKS bounded implementation worker.', './agents/implementation-worker.toml', ['Builder', 'Mason']) },
|
|
446
|
+
{ table: 'agents.db_safety_reviewer', text: agentConfigBlock('db_safety_reviewer', 'Read-only DB safety reviewer.', './agents/db-safety-reviewer.toml', ['Sentinel', 'Ledger']) },
|
|
447
|
+
{ table: 'agents.qa_reviewer', text: agentConfigBlock('qa_reviewer', 'Read-only QA reviewer.', './agents/qa-reviewer.toml', ['Verifier', 'Scout']) },
|
|
448
|
+
{ table: 'profiles.sks-task-low', text: profileConfigBlock('sks-task-low', 'low') },
|
|
449
|
+
{ table: 'profiles.sks-task-medium', text: profileConfigBlock('sks-task-medium', 'medium') },
|
|
450
|
+
{ table: 'profiles.sks-logic-high', text: profileConfigBlock('sks-logic-high', 'high') },
|
|
451
|
+
{ table: 'profiles.sks-fast-high', text: profileConfigBlock('sks-fast-high', 'high') },
|
|
452
|
+
{ table: 'profiles.sks-research-xhigh', text: profileConfigBlock('sks-research-xhigh', 'xhigh') },
|
|
453
|
+
{ table: 'profiles.sks-research', text: profileConfigBlock('sks-research', 'xhigh', { approval: 'never' }) },
|
|
454
|
+
{ table: 'profiles.sks-team', text: profileConfigBlock('sks-team', 'high') },
|
|
455
|
+
{ table: 'profiles.sks-mad-high', text: profileConfigBlock('sks-mad-high', 'high', { sandbox: 'danger-full-access', approvalsReviewer: 'auto_review' }) },
|
|
456
|
+
{
|
|
457
|
+
table: 'auto_review',
|
|
458
|
+
text: '[auto_review]\npolicy = "Deny destructive database operations, credential exfiltration, persistent security weakening, broad file deletion, writes outside the workspace, and unrequested fallback implementation code unless explicitly authorized by the user or sealed decision contract."'
|
|
459
|
+
},
|
|
460
|
+
{ table: 'profiles.sks-default', text: profileConfigBlock('sks-default', 'high') }
|
|
461
|
+
];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function agentConfigBlock(table, description, configFile, nicknames = []) {
|
|
465
|
+
return [
|
|
466
|
+
`[agents.${table}]`,
|
|
467
|
+
`description = "${description}"`,
|
|
468
|
+
`config_file = "${configFile}"`,
|
|
469
|
+
`nickname_candidates = [${nicknames.map((name) => `"${name}"`).join(', ')}]`
|
|
470
|
+
].join('\n');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function profileConfigBlock(profile, effort, opts = {}) {
|
|
474
|
+
return [
|
|
475
|
+
`[profiles.${profile}]`,
|
|
476
|
+
'model = "gpt-5.5"',
|
|
477
|
+
`approval_policy = "${opts.approval || 'on-request'}"`,
|
|
478
|
+
...(opts.approvalsReviewer ? [`approvals_reviewer = "${opts.approvalsReviewer}"`] : []),
|
|
479
|
+
`sandbox_mode = "${opts.sandbox || 'workspace-write'}"`,
|
|
480
|
+
`model_reasoning_effort = "${effort}"`
|
|
481
|
+
].join('\n');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function upsertTomlTableKey(text, table, line) {
|
|
485
|
+
const key = String(line).split('=')[0].trim();
|
|
486
|
+
let lines = String(text || '').split('\n');
|
|
487
|
+
if (lines.length === 1 && lines[0] === '') lines = [];
|
|
488
|
+
const header = `[${table}]`;
|
|
489
|
+
let start = lines.findIndex((x) => x.trim() === header);
|
|
490
|
+
if (start === -1) {
|
|
491
|
+
const prefix = lines.length && lines[lines.length - 1].trim() ? ['', header, line] : [header, line];
|
|
492
|
+
return [...lines, ...prefix].join('\n').replace(/\n{3,}/g, '\n\n');
|
|
493
|
+
}
|
|
494
|
+
let end = lines.length;
|
|
495
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
496
|
+
if (/^\s*\[.+\]\s*$/.test(lines[i])) {
|
|
497
|
+
end = i;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
for (let i = start + 1; i < end; i++) {
|
|
502
|
+
if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[i])) {
|
|
503
|
+
lines[i] = line;
|
|
504
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
505
|
+
}
|
|
426
506
|
}
|
|
507
|
+
lines.splice(start + 1, 0, line);
|
|
508
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function upsertTomlTable(text, table, block) {
|
|
512
|
+
let lines = String(text || '').trimEnd().split('\n');
|
|
513
|
+
if (lines.length === 1 && lines[0] === '') lines = [];
|
|
514
|
+
const header = `[${table}]`;
|
|
515
|
+
const start = lines.findIndex((x) => x.trim() === header);
|
|
516
|
+
const blockLines = String(block || '').trim().split('\n');
|
|
517
|
+
if (start === -1) {
|
|
518
|
+
return [...lines, ...(lines.length ? [''] : []), ...blockLines].join('\n').replace(/\n{3,}/g, '\n\n');
|
|
519
|
+
}
|
|
520
|
+
let end = lines.length;
|
|
521
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
522
|
+
if (/^\s*\[.+\]\s*$/.test(lines[i])) {
|
|
523
|
+
end = i;
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
lines.splice(start, end - start, ...blockLines);
|
|
528
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
529
|
+
}
|
|
427
530
|
|
|
428
531
|
const currentState = path.join(sine, 'state', 'current.json');
|
|
429
532
|
if (!(await exists(currentState)) || opts.force) {
|
|
@@ -441,26 +544,9 @@ export async function initProject(root, opts = {}) {
|
|
|
441
544
|
created.push('AGENTS.md managed block');
|
|
442
545
|
}
|
|
443
546
|
|
|
444
|
-
await writeTextAtomic(path.join(root, '.codex', 'config.toml'), `[features]\ncodex_hooks = true\nmulti_agent = true\n\n[agents]\nmax_threads = 6\nmax_depth = 1\n\n${context7ConfigToml()}\n[agents.analysis_scout]\ndescription = "Read-only SKS scout."\nconfig_file = "./agents/analysis-scout.toml"\nnickname_candidates = ["Scout", "Mapper"]\n\n[agents.team_consensus]\ndescription = "SKS planning/debate agent."\nconfig_file = "./agents/team-consensus.toml"\nnickname_candidates = ["Consensus", "Atlas"]\n\n[agents.implementation_worker]\ndescription = "SKS bounded implementation worker."\nconfig_file = "./agents/implementation-worker.toml"\nnickname_candidates = ["Builder", "Mason"]\n\n[agents.db_safety_reviewer]\ndescription = "Read-only DB safety reviewer."\nconfig_file = "./agents/db-safety-reviewer.toml"\nnickname_candidates = ["Sentinel", "Ledger"]\n\n[agents.qa_reviewer]\ndescription = "Read-only QA reviewer."\nconfig_file = "./agents/qa-reviewer.toml"\nnickname_candidates = ["Verifier", "Scout"]\n\n[profiles.sks-task-medium]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "medium"\n\n[profiles.sks-logic-high]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "high"\n\n[profiles.sks-research-xhigh]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "xhigh"\n\n[profiles.sks-research]\nmodel = "gpt-5.5"\napproval_policy = "never"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "xhigh"\n\n[profiles.sks-team]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "high"\n\n[profiles.sks-mad-high]
|
|
445
|
-
model = "gpt-5.5"
|
|
446
|
-
approval_policy = "on-request"
|
|
447
|
-
approvals_reviewer = "auto_review"
|
|
448
|
-
sandbox_mode = "danger-full-access"
|
|
449
|
-
model_reasoning_effort = "high"
|
|
450
|
-
|
|
451
|
-
[auto_review]
|
|
452
|
-
policy = "Deny destructive database operations, credential exfiltration, persistent security weakening, broad file deletion, writes outside the workspace, and unrequested fallback implementation code unless explicitly authorized by the user or sealed decision contract."
|
|
453
|
-
|
|
454
|
-
[profiles.sks-default]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "medium"\n`);
|
|
455
547
|
const generatedCodexConfigPath = path.join(root, '.codex', 'config.toml');
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
generatedCodexConfig = generatedCodexConfig.replace(
|
|
459
|
-
'[profiles.sks-task-medium]',
|
|
460
|
-
'[profiles.sks-task-low]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "low"\n\n[profiles.sks-task-medium]'
|
|
461
|
-
);
|
|
462
|
-
await writeTextAtomic(generatedCodexConfigPath, generatedCodexConfig);
|
|
463
|
-
}
|
|
548
|
+
const existingCodexConfig = await readText(generatedCodexConfigPath, '');
|
|
549
|
+
await writeTextAtomic(generatedCodexConfigPath, mergeManagedCodexConfigToml(existingCodexConfig));
|
|
464
550
|
created.push('.codex/config.toml');
|
|
465
551
|
|
|
466
552
|
await writeTextAtomic(path.join(root, '.codex', 'SNEAKOSCOPE.md'), codexAppQuickReference(installScope, hookCommandPrefix));
|
package/src/core/tmux-ui.mjs
CHANGED
|
@@ -14,6 +14,19 @@ export const SKS_TMUX_LOGO = [
|
|
|
14
14
|
'Sneakoscope Codex tmux'
|
|
15
15
|
].join('\n');
|
|
16
16
|
|
|
17
|
+
export const DEFAULT_SKS_CODEX_MODEL = 'gpt-5.5';
|
|
18
|
+
export const DEFAULT_SKS_CODEX_REASONING = 'high';
|
|
19
|
+
|
|
20
|
+
export function defaultCodexLaunchArgs(env = process.env) {
|
|
21
|
+
if (/^(0|false|off|none)$/i.test(String(env.SKS_CODEX_FAST_HIGH || '').trim())) return [];
|
|
22
|
+
const model = String(env.SKS_CODEX_MODEL || DEFAULT_SKS_CODEX_MODEL).trim();
|
|
23
|
+
const effort = String(env.SKS_CODEX_REASONING || DEFAULT_SKS_CODEX_REASONING).trim();
|
|
24
|
+
const args = [];
|
|
25
|
+
if (model) args.push('--model', model);
|
|
26
|
+
if (effort) args.push('-c', `model_reasoning_effort="${effort}"`);
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
export function sanitizeTmuxSessionName(input) {
|
|
18
31
|
const base = String(input || 'sks').trim().replace(/[^A-Za-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
19
32
|
return (base || 'sks').slice(0, 80);
|
|
@@ -177,7 +190,7 @@ export async function buildTmuxLaunchPlan(opts = {}) {
|
|
|
177
190
|
const codex = opts.codex || await getCodexInfo().catch(() => ({}));
|
|
178
191
|
const tmux = opts.tmux || await tmuxReadiness(opts);
|
|
179
192
|
const app = opts.app || await codexAppIntegrationStatus({ codex });
|
|
180
|
-
const codexArgs = Array.isArray(opts.codexArgs) ? opts.codexArgs :
|
|
193
|
+
const codexArgs = Array.isArray(opts.codexArgs) ? opts.codexArgs : defaultCodexLaunchArgs(opts.env || process.env);
|
|
181
194
|
return {
|
|
182
195
|
root,
|
|
183
196
|
session,
|
|
@@ -48,7 +48,11 @@ async function runtimeDriftStatus(root, packageVersion) {
|
|
|
48
48
|
if (!packageVersion || process.env.SKS_RUNTIME_DRIFT_CHECK === '0') {
|
|
49
49
|
return { ok: true, checked: false, reason: packageVersion ? 'disabled' : 'package_json_version_missing' };
|
|
50
50
|
}
|
|
51
|
-
const
|
|
51
|
+
const localBin = path.join(root, 'bin', 'sks.mjs');
|
|
52
|
+
const useLocalBin = await exists(localBin);
|
|
53
|
+
const command = useLocalBin ? process.execPath : 'sks';
|
|
54
|
+
const args = useLocalBin ? [localBin, '--version'] : ['--version'];
|
|
55
|
+
const result = await runProcess(command, args, {
|
|
52
56
|
cwd: root,
|
|
53
57
|
timeoutMs: 5000,
|
|
54
58
|
maxOutputBytes: 16 * 1024,
|
|
@@ -68,6 +72,7 @@ async function runtimeDriftStatus(root, packageVersion) {
|
|
|
68
72
|
return {
|
|
69
73
|
ok: comparison >= 0,
|
|
70
74
|
checked: true,
|
|
75
|
+
command: [command, ...args].join(' '),
|
|
71
76
|
runtime_version: runtimeVersion,
|
|
72
77
|
package_version: packageVersion,
|
|
73
78
|
relation: comparison === 0 ? 'same' : (comparison > 0 ? 'runtime_newer' : 'runtime_older')
|
|
@@ -83,7 +88,7 @@ export async function runVersionPreCommit(root, opts = {}) {
|
|
|
83
88
|
if (!pkg?.version) return { ok: true, skipped: true, reason: 'package_json_version_missing' };
|
|
84
89
|
const git = await gitPaths(root);
|
|
85
90
|
if (!git.ok) return { ok: true, skipped: true, reason: git.reason || 'not_git' };
|
|
86
|
-
return withVersionLock(git.common_dir, async () =>
|
|
91
|
+
return withVersionLock(git.common_dir, async () => verifyProjectVersion(root, { ...opts, policy, git }));
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
export async function bumpProjectVersion(root, opts = {}) {
|
|
@@ -145,6 +150,43 @@ export async function bumpProjectVersion(root, opts = {}) {
|
|
|
145
150
|
};
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
export async function verifyProjectVersion(root, opts = {}) {
|
|
154
|
+
const git = opts.git || await gitPaths(root);
|
|
155
|
+
const pkgPath = path.join(root, 'package.json');
|
|
156
|
+
const pkg = await readJson(pkgPath);
|
|
157
|
+
const current = parseSemver(pkg.version);
|
|
158
|
+
if (!current) return { ok: false, reason: `Unsupported package.json version: ${pkg.version}` };
|
|
159
|
+
const version = formatSemver(current);
|
|
160
|
+
const sourceVersion = await syncSourcePackageVersion(root, version);
|
|
161
|
+
const synced = await syncPackageLockVersions(root, version);
|
|
162
|
+
if (!await changelogHasVersionSection(root, version)) {
|
|
163
|
+
return { ok: false, reason: 'changelog_section_missing', version, expected: `## [${version}]` };
|
|
164
|
+
}
|
|
165
|
+
const staged = await stageVersionFiles(root, [...synced.files, ...sourceVersion.files]);
|
|
166
|
+
if (!staged.ok) return { ok: false, reason: 'git_add_version_files_failed', stderr: staged.stderr };
|
|
167
|
+
const statePath = git.ok ? path.join(git.common_dir, VERSION_STATE_FILE) : null;
|
|
168
|
+
if (statePath) {
|
|
169
|
+
await writeJsonAtomic(statePath, {
|
|
170
|
+
schema_version: 1,
|
|
171
|
+
last_version: version,
|
|
172
|
+
updated_at: nowIso(),
|
|
173
|
+
pid: process.pid,
|
|
174
|
+
mode: 'verify',
|
|
175
|
+
changed: Boolean(synced.files.length || sourceVersion.files.length)
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
ok: true,
|
|
180
|
+
changed: Boolean(synced.files.length || sourceVersion.files.length),
|
|
181
|
+
version,
|
|
182
|
+
previous_version: version,
|
|
183
|
+
synced_files: [...synced.relative_files, ...sourceVersion.relative_files],
|
|
184
|
+
staged_files: staged.relative_files,
|
|
185
|
+
lock_scope: git.common_dir,
|
|
186
|
+
mode: 'verify'
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
148
190
|
async function versionPolicy(root) {
|
|
149
191
|
const policy = await readJson(path.join(root, '.sneakoscope', 'policy.json'), {});
|
|
150
192
|
return {
|
|
@@ -252,6 +294,13 @@ async function syncChangelogVersionSection(root, version) {
|
|
|
252
294
|
return { files: [file], relative_files: [path.relative(root, file)] };
|
|
253
295
|
}
|
|
254
296
|
|
|
297
|
+
async function changelogHasVersionSection(root, version) {
|
|
298
|
+
const file = path.join(root, 'CHANGELOG.md');
|
|
299
|
+
const text = await readFileMaybe(file);
|
|
300
|
+
const sectionRe = new RegExp(`^##\\s+\\[${escapeRegExp(version)}\\]\\s+-\\s+\\d{4}-\\d{2}-\\d{2}\\s*$`, 'm');
|
|
301
|
+
return sectionRe.test(text);
|
|
302
|
+
}
|
|
303
|
+
|
|
255
304
|
async function stageVersionFiles(root, files) {
|
|
256
305
|
const existing = [];
|
|
257
306
|
for (const file of files) if (await exists(file)) existing.push(path.relative(root, file));
|