sneakoscope 0.6.24 → 0.6.25

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 CHANGED
@@ -30,6 +30,7 @@ Sneakoscope Codex is for developers who want Codex CLI to keep working until a g
30
30
  - **Database-safe autonomous coding**: destructive SQL, unsafe Supabase MCP writes, production DB mutation, and risky migration flows are blocked or surfaced early.
31
31
  - **Harness self-protection**: after setup, installed SKS control files are locked against LLM tool edits, with a source-repo-only exception for Sneakoscope engine development.
32
32
  - **Other-harness conflict gate**: OMX/DCodex-style Codex harness traces block npm install and setup until a human-approved cleanup is performed.
33
+ - **Automatic project versioning**: every commit can carry a unique patch version bump, with package-lock sync and a Git common-dir lock for parallel workers.
33
34
  - **Honest completion gates**: H-Proof and Honest Mode require evidence before the agent claims the work is complete.
34
35
  - **TriWiki context-tracking SSOT**: structured wiki packs, visual coordinate anchors, and bounded memory help long-running work survive context pressure without relying on lossy summaries.
35
36
 
@@ -42,7 +43,7 @@ npm i -g sneakoscope
42
43
  sks
43
44
  ```
44
45
 
45
- `npm i -g sneakoscope` prints the next command without opening an interactive prompt, so CI and agent installs do not hang. During postinstall, SKS blocks installation if OMX, DCodex, or their global/repo-level traces are detected; the output includes a GPT-5.5 high cleanup prompt and installation cannot continue unless a human approves removal. If no conflicting harness exists, SKS checks whether the `sks` command is available, best-effort creates a command shim in a writable PATH directory when needed, and best-effort installs the Context7 MCP globally when Codex CLI is available. Run `sks` in a real terminal to open the setup UI. The UI asks whether this project should use the global install or a project-only install, then offers to run setup, doctor, and selftest.
46
+ `npm i -g sneakoscope` prints setup guidance without making npm output look like a crash. If OMX, DCodex, or their global/repo-level traces are detected during postinstall, npm installation can still finish, but SKS clearly reports that `sks setup` and `sks doctor --fix` are blocked until a human-approved cleanup happens. In an interactive terminal, postinstall asks whether to show the GPT-5.5 high cleanup prompt now; in CI or agent installs, it prints `sks conflicts prompt` for later. If no conflicting harness exists, SKS checks whether the `sks` command is available, best-effort creates a command shim in a writable PATH directory when needed, and best-effort installs the Context7 MCP globally when Codex CLI is available. Run `sks` in a real terminal to open the setup UI. The UI asks whether this project should use the global install or a project-only install, then offers to run setup, doctor, and selftest.
46
47
 
47
48
  Default non-interactive setup:
48
49
 
@@ -103,6 +104,7 @@ sks quickstart
103
104
  sks codex-app
104
105
  sks dollar-commands
105
106
  sks context7 tools
107
+ sks versioning status
106
108
  sks df
107
109
  sks aliases
108
110
  ```
@@ -163,6 +165,24 @@ SKS uses the official Codex hook behavior: `UserPromptSubmit` can inject additio
163
165
 
164
166
  After setup, SKS writes `.sneakoscope/harness-guard.json`. Hooks block LLM tool calls that try to edit installed harness control files such as `.codex/hooks.json`, `.codex/config.toml`, `.codex/SNEAKOSCOPE.md`, `.agents/skills/`, `.codex/agents/`, `.sneakoscope/manifest.json`, `.sneakoscope/policy.json`, `.sneakoscope/db-safety.json`, `AGENTS.md`, or `node_modules/sneakoscope`. The only automatic exception is the Sneakoscope engine source repository itself, detected from `package.json` name `sneakoscope` plus `bin/sks.mjs` and `src/core/*`.
165
167
 
168
+ ## Project Versioning
169
+
170
+ SKS setup installs a managed Git `pre-commit` hook for projects with `package.json`. On every commit, SKS bumps the project patch version before Git writes the commit, syncs `package-lock.json` and `npm-shrinkwrap.json` when present, and stages those version files into the same commit.
171
+
172
+ Multiple workers and worktrees share a version lock in the Git common directory. If another worker already used a version, the next commit automatically bumps above the last seen version instead of reusing it.
173
+
174
+ ```bash
175
+ sks versioning status
176
+ sks versioning bump
177
+ sks versioning hook
178
+ ```
179
+
180
+ The bypass is intentionally explicit and conversation-local:
181
+
182
+ ```bash
183
+ SKS_DISABLE_VERSIONING=1 git commit ...
184
+ ```
185
+
166
186
  Inside Codex App, you can ask the agent to use the local SKS control surface, for example:
167
187
 
168
188
  ```text
@@ -264,6 +284,7 @@ sks research run latest --max-cycles 3
264
284
  - **Forced subagent execution policy**: code-changing work first surfaces SKS status context, then defaults to parallel worker subagents when independent write scopes exist; the parent orchestrator owns integration and verification.
265
285
  - **AutoResearch loop**: open-ended improvement tasks use a small experiment cycle: program, hypothesis, experiment, metric, keep/discard, falsification, and honest conclusion.
266
286
  - **Update-aware hooks**: before work, SKS checks for a newer published package and asks whether to update now or skip for the current conversation only.
287
+ - **Automatic project versioning**: setup installs a pre-commit guard that bumps `package.json` patch versions, syncs lockfiles, stages them, and avoids duplicate versions across parallel workers.
267
288
  - **Honest Mode finish**: final answers must include an evidence-aware verification pass before claiming the goal is complete.
268
289
  - **Fast DF mode**: `$DF` handles small design/content edits like color, copy, labels, spacing, and translation without unnecessary Ralph, Research, or evaluation loops.
269
290
  - **Database guard**: destructive DB operations, production writes, unsafe Supabase MCP configuration, and direct live SQL mutations are blocked or warned on.
@@ -305,42 +326,9 @@ Team mode uses Codex subagents/custom agents as an orchestration protocol rather
305
326
 
306
327
  For code-changing work, generated SKS rules tell Codex to surface visible route, guard, write-scope, and verification status before editing. When the work has independent, non-overlapping write scopes, Codex should spawn worker subagents in parallel by default; the parent keeps urgent blockers local, assigns ownership, integrates results, and runs final verification.
307
328
 
308
- Team missions default to `executor:3 reviewer:1 user:1 planner:1`. Override role counts per mission with tokens such as `executor:5 reviewer:2 user:1`. `executor:N` means SKS creates exactly N read-only analysis scouts first, exactly N debate participants next, and then a separate N-person executor development team. `--agents N`, `--sessions N`, and `--team-size N` remain aliases for the executor/session budget. `--max-agents` uses the configured default maximum of 6 sessions/agents. The parent orchestrator is not counted.
329
+ Team missions default to `executor:3 reviewer:1 user:1 planner:1`. Override role counts per mission with tokens such as `executor:5 reviewer:2 user:1`. `executor:N` creates N read-only analysis scouts, N debate participants, and then a separate N-person executor development team. The parent orchestrator is not counted.
309
330
 
310
- ```text
311
- parallel analysis scouts
312
- -> spawn exactly N read-only analysis_scout_N agents
313
- -> split repo, docs, tests, API, DB-risk, UX-friction, and implementation-surface investigation
314
- -> write source-backed findings and TriWiki-ready claims to team-analysis.md
315
-
316
- TriWiki refresh
317
- -> parent orchestrator runs sks wiki pack
318
- -> parent validates .sneakoscope/wiki/context-pack.json with sks wiki validate
319
- -> later debate and implementation handoffs use refreshed anchor-first context
320
-
321
- debate team
322
- -> spawn exactly N role personas for stubborn users, capable executor voices, strict reviewers, and planners
323
- -> map user inconvenience, code paths, risks, DB safety, tests, and options
324
- -> synthesize one agreed objective with constraints and acceptance criteria
325
- -> close debate agents
326
-
327
- fresh development team
328
- -> create a separate N-person executor_N developer team
329
- -> assign disjoint write scopes to executor_N developers
330
- -> run executor_N work in parallel only when ownership does not overlap
331
- -> strict reviewer_N and user_N personas check correctness, evidence, and practical friction
332
- -> parent orchestrator integrates, verifies, and reports evidence
333
-
334
- live transcript
335
- -> mirror every useful agent status, debate result, handoff, and review finding
336
- -> keep team-live.md readable inside Codex App
337
- -> keep team-transcript.jsonl machine-readable for tails, dashboards, and future tooling
338
-
339
- context tracking
340
- -> use TriWiki as the SSOT for long-running mission context and team handoffs
341
- -> refresh .sneakoscope/wiki/context-pack.json with sks wiki pack when context changes
342
- -> validate the pack with sks wiki validate .sneakoscope/wiki/context-pack.json
343
- ```
331
+ The pipeline is scout-first: parallel analysis, TriWiki refresh, planning debate, consensus, fresh parallel implementation, review, integration, and Honest Mode evidence.
344
332
 
345
333
  Create a Team mission:
346
334
 
@@ -356,7 +344,7 @@ Inside Codex App, use:
356
344
  $Team executor:5 run parallel analysis scouts, refresh TriWiki, agree on the best plan, close the debate team, then implement with a fresh development team
357
345
  ```
358
346
 
359
- The generated Team artifacts are:
347
+ Key Team artifacts:
360
348
 
361
349
  ```text
362
350
  .sneakoscope/missions/<MISSION_ID>/team-plan.json
@@ -369,8 +357,6 @@ The generated Team artifacts are:
369
357
  .codex/agents/analysis-scout.toml
370
358
  .codex/agents/team-consensus.toml
371
359
  .codex/agents/implementation-worker.toml
372
- .codex/agents/db-safety-reviewer.toml
373
- .codex/agents/qa-reviewer.toml
374
360
  ```
375
361
 
376
362
  Live team visibility commands:
@@ -425,70 +411,45 @@ All terminal examples below use `sks`, but the same commands can be run with the
425
411
 
426
412
  ```bash
427
413
  sks help [topic]
428
- sks update-check [--json]
429
414
  sks wizard
430
415
  sks commands [--json]
431
- sks usage [install|setup|team|ralph|research|db|codex-app|df|dollar|context7|pipeline|reasoning|eval|gx|wiki]
416
+ sks usage [topic]
432
417
  sks quickstart
433
418
  sks codex-app
434
419
  sks dollar-commands [--json]
435
420
  sks df
436
- sks context7 check|setup|tools|resolve|docs|evidence ...
437
- sks pipeline status|resume [--json]
438
- sks guard check [--json]
439
- sks conflicts check|prompt [--json]
440
- sks reasoning ["prompt"] [--json]
441
- sks aliases
442
421
 
443
422
  sks --help
444
423
  sneakoscope --help
445
424
 
446
425
  sks setup [--install-scope global|project] [--local-only] [--force] [--json]
447
- sks fix-path [--install-scope global|project] [--json]
448
426
  sks doctor [--fix] [--local-only] [--json] [--install-scope global|project]
449
- sks init [--force] [--local-only] [--install-scope global|project]
450
427
  sks selftest [--mock]
428
+ sks versioning status|bump|hook
451
429
 
452
430
  sks ralph prepare "task"
453
431
  sks ralph answer <mission-id|latest> <answers.json>
454
432
  sks ralph run <mission-id|latest> [--mock] [--max-cycles N]
455
- sks ralph status <mission-id|latest>
456
433
 
457
434
  sks research prepare "topic" [--depth frontier]
458
435
  sks research run <mission-id|latest> [--mock] [--max-cycles N]
459
- sks research status <mission-id|latest>
460
436
 
461
- sks db policy
462
437
  sks db scan [--migrations] [--json]
463
- sks db mcp-config --project-ref <ref> [--features database,docs]
464
- sks db classify --sql "DROP TABLE users"
465
- sks db classify --command "supabase db reset"
466
438
  sks db check --sql "SELECT * FROM users LIMIT 10"
467
439
  sks db check --command "supabase db reset"
468
- sks db check --file ./migration.sql
469
-
470
- sks eval run [--json] [--out report.json] [--iterations N]
471
- sks eval compare --baseline old.json --candidate new.json [--json]
472
- sks eval thresholds
473
440
 
474
- sks wiki coords --rgba 12,34,56,255
441
+ sks team "task" [executor:5 reviewer:2 user:1] [--json]
442
+ sks team log|tail|watch|status [mission-id|latest]
475
443
  sks wiki pack [--json] [--role worker|verifier] [--max-anchors N]
476
444
  sks wiki validate [context-pack.json]
477
-
445
+ sks context7 check|setup|tools|docs ...
446
+ sks pipeline status|resume [--json]
447
+ sks guard check [--json]
448
+ sks conflicts check|prompt [--json]
449
+ sks eval run|compare|thresholds ...
478
450
  sks hproof check [mission-id|latest]
479
- sks team "task" [executor:5 reviewer:2 user:1] [--json]
480
- sks team log|tail|watch|status [mission-id|latest]
481
- sks team event [mission-id|latest] --agent <name> --phase <phase> --message "..."
482
- sks gx init [name]
483
- sks gx render [name] [--format svg|html|all]
484
- sks gx validate [name]
485
- sks gx drift [name]
486
- sks gx snapshot [name]
487
- sks profile show
488
- sks profile set <model>
451
+ sks gx init|render|validate|drift|snapshot [name]
489
452
  sks gc [--dry-run] [--json]
490
- sks memory [--dry-run] [--json]
491
- sks stats [--json]
492
453
  ```
493
454
 
494
455
  `sks memory` is currently an alias for garbage collection/retention handling.
@@ -751,7 +712,7 @@ Storage is intentionally bounded:
751
712
  - `sks gc` compacts oversized JSONL logs and prunes old artifacts
752
713
  - `sks stats` reports package and `.sneakoscope` storage size
753
714
 
754
- See [docs/PERFORMANCE.md](docs/PERFORMANCE.md) for the detailed resource policy.
715
+ See the [resource policy](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/PERFORMANCE.md) for the detailed storage and leak policy.
755
716
 
756
717
  ## Visual Cartridges
757
718
 
@@ -837,11 +798,10 @@ src/core/init.mjs project bootstrap and hook/skill installation
837
798
  src/core/research.mjs research-mode plan, novelty ledger, and gate helpers
838
799
  src/core/retention.mjs storage report and garbage collection policy
839
800
  src/core/triwiki-attention.mjs
840
- docs/PERFORMANCE.md resource and leak policy
841
801
  crates/sks-core/ optional Rust helper source, not shipped in npm package
842
802
  ```
843
803
 
844
- The published npm package is allowlisted to `bin`, `src`, `docs`, `README.md`, and `LICENSE`; `.sneakoscope`, `.codex`, `.agents`, Rust sources, archives, and local state are excluded.
804
+ The published npm package is allowlisted to `bin`, `src`, `README.md`, and `LICENSE`; `.sneakoscope`, `.codex`, `.agents`, `docs`, Rust sources, archives, and local state are excluded.
845
805
 
846
806
  ## Development
847
807
 
@@ -856,7 +816,7 @@ npm run doctor
856
816
 
857
817
  `npm run repo-audit` checks tracked files for risky local paths and high-confidence secret material such as private keys, npm/GitHub/OpenAI-style tokens, local MCP configs, DB dumps, and credential files. It is included in `release:check` and `prepublishOnly`. The package intentionally does not define `prepack`; GitHub installs should not trigger npm's heavier git-dependency preparation path for normal users.
858
818
 
859
- `npm run sizecheck` blocks accidental package bloat during `release:check`, `publish:dry`, and `npm publish`. Defaults: packed tarball `<=132 KiB`, unpacked package `<=470 KiB`, package files `<=40`, and each tracked file `<=256 KiB`. Override only for an intentional release with `SKS_MAX_PACK_BYTES`, `SKS_MAX_UNPACKED_BYTES`, `SKS_MAX_PACK_FILES`, or `SKS_MAX_TRACKED_FILE_BYTES`.
819
+ `npm run sizecheck` blocks accidental package bloat during `release:check`, `publish:dry`, and `npm publish`. Defaults: packed tarball `<=136 KiB`, unpacked package `<=500 KiB`, package files `<=40`, and each tracked file `<=256 KiB`. Override only for an intentional release with `SKS_MAX_PACK_BYTES`, `SKS_MAX_UNPACKED_BYTES`, `SKS_MAX_PACK_FILES`, or `SKS_MAX_TRACKED_FILE_BYTES`.
860
820
 
861
821
  `npm run selftest` uses the mock path and does not call a model. Live Ralph runs require a working Codex CLI installation and authentication.
862
822
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "Sneakoscope Codex",
4
- "version": "0.6.24",
4
+ "version": "0.6.25",
5
5
  "description": "Sneakoscope Codex: update-aware, database-safe Codex CLI harness with multi-agent Team orchestration, Ralph no-question execution, autoresearch-style loops, and H-Proof gates.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -23,7 +23,6 @@
23
23
  "files": [
24
24
  "bin",
25
25
  "src",
26
- "docs",
27
26
  "README.md",
28
27
  "LICENSE"
29
28
  ],
package/src/cli/main.mjs CHANGED
@@ -16,6 +16,7 @@ import { classifySql, classifyCommand, loadDbSafetyPolicy, safeSupabaseMcpConfig
16
16
  import { checkHarnessModification, harnessGuardStatus, isHarnessSourceProject } from '../core/harness-guard.mjs';
17
17
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
18
18
  import { context7Docs, context7Resolve, context7Text, context7Tools } from '../core/context7-client.mjs';
19
+ import { installVersionGitHook, runVersionPreCommit, versioningStatus } from '../core/version-manager.mjs';
19
20
  import { rustInfo } from '../core/rust-accelerator.mjs';
20
21
  import { renderCartridge, validateCartridge, driftCartridge, snapshotCartridge } from '../core/gx-renderer.mjs';
21
22
  import { DEFAULT_EVAL_THRESHOLDS, compareEvaluationReports, defaultEvaluationScenario, runEvaluationBenchmark } from '../core/evaluation.mjs';
@@ -57,6 +58,7 @@ export async function main(args) {
57
58
  if (cmd === 'pipeline') return pipeline(sub, rest);
58
59
  if (cmd === 'guard') return guard(sub, rest);
59
60
  if (cmd === 'conflicts') return conflicts(sub, rest);
61
+ if (cmd === 'versioning') return versioning(sub, rest);
60
62
  if (cmd === 'reasoning') return reasoningCommand(tail);
61
63
  if (cmd === 'aliases') return aliases();
62
64
  if (cmd === 'setup') return setup(tail);
@@ -101,6 +103,7 @@ Usage:
101
103
  sks pipeline status|resume [--json]
102
104
  sks guard check [--json]
103
105
  sks conflicts check|prompt [--json]
106
+ sks versioning status|bump|pre-commit [--json]
104
107
  sks reasoning ["prompt"] [--json]
105
108
  sks aliases
106
109
  sks setup [--install-scope global|project] [--local-only] [--force] [--json]
@@ -161,10 +164,7 @@ async function postinstall() {
161
164
  const installRoot = path.resolve(process.env.INIT_CWD || process.cwd());
162
165
  const conflictScan = await scanHarnessConflicts(installRoot);
163
166
  if (conflictScan.hard_block) {
164
- console.error('\nSneakoscope Codex install blocked.');
165
- console.error(formatHarnessConflictReport(conflictScan));
166
- console.error('\nRun the cleanup prompt above in Codex App with GPT-5.5 high mode, then rerun npm install.');
167
- process.exitCode = 1;
167
+ await postinstallHarnessConflictNotice(conflictScan);
168
168
  return;
169
169
  }
170
170
  console.log('\nSneakoscope Codex installed.');
@@ -184,6 +184,40 @@ async function postinstall() {
184
184
  console.log('Project-only setup: `sks wizard` -> choose project, or `npx sks setup --install-scope project`.\n');
185
185
  }
186
186
 
187
+ async function postinstallHarnessConflictNotice(conflictScan) {
188
+ console.log('\nSneakoscope Codex package installed, but SKS setup is blocked.');
189
+ console.log(formatHarnessConflictReport(conflictScan, { includePrompt: false }));
190
+ console.log('\nWhat this means: npm can finish installing the package, but `sks setup` and `sks doctor --fix` will refuse to activate SKS until the conflicting harness is removed with human approval.');
191
+ console.log('No files were removed by postinstall.');
192
+ console.log('Cleanup requires a human-approved Codex App session. Recommended model: GPT-5.5, reasoning: high.');
193
+ if (shouldAskPostinstallQuestion()) {
194
+ const answer = await askPostinstallQuestion('Show the cleanup prompt now? [y/N] ');
195
+ if (/^(y|yes|예|네|응)$/i.test(answer.trim())) {
196
+ console.log('\nCleanup prompt:\n');
197
+ console.log(llmHarnessCleanupPrompt(conflictScan));
198
+ } else {
199
+ console.log('Cleanup prompt skipped. You can print it later with: sks conflicts prompt');
200
+ }
201
+ } else {
202
+ console.log('Print the cleanup prompt later with: sks conflicts prompt');
203
+ }
204
+ console.log('After approved cleanup, rerun: sks setup && sks doctor --fix && sks selftest --mock\n');
205
+ }
206
+
207
+ function shouldAskPostinstallQuestion() {
208
+ if (process.env.SKS_POSTINSTALL_PROMPT === '1') return true;
209
+ return Boolean(input.isTTY && output.isTTY && process.env.CI !== 'true' && process.env.SKS_POSTINSTALL_NO_PROMPT !== '1');
210
+ }
211
+
212
+ async function askPostinstallQuestion(question) {
213
+ const rl = readline.createInterface({ input, output });
214
+ try {
215
+ return await rl.question(question);
216
+ } finally {
217
+ rl.close();
218
+ }
219
+ }
220
+
187
221
  async function ensureSksCommandDuringInstall(opts = {}) {
188
222
  if (process.env.SKS_SKIP_POSTINSTALL_SHIM === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_SHIM=1' };
189
223
  const pathEnv = opts.pathEnv ?? process.env.PATH ?? '';
@@ -566,6 +600,55 @@ async function conflicts(sub = 'check', args = []) {
566
600
  if (scan.conflicts.length) console.log(formatHarnessConflictReport(scan));
567
601
  }
568
602
 
603
+ async function versioning(sub = 'status', args = []) {
604
+ const root = await projectRoot();
605
+ const action = sub || 'status';
606
+ if (action === 'status' || action === 'check') {
607
+ const status = await versioningStatus(root);
608
+ if (flag(args, '--json')) return console.log(JSON.stringify(status, null, 2));
609
+ console.log('SKS Project Versioning\n');
610
+ console.log(`Enabled: ${status.enabled ? 'yes' : 'no'}${status.reason ? ` (${status.reason})` : ''}`);
611
+ console.log(`Version: ${status.package_version || 'none'}`);
612
+ console.log(`Bump: ${status.bump || 'patch'}`);
613
+ console.log(`Hook: ${status.hook_installed ? 'installed' : 'missing'}${status.hook_path ? ` ${status.hook_path}` : ''}`);
614
+ console.log(`Last seen: ${status.last_version || 'none'}`);
615
+ if (!status.ok) console.log('Run: sks doctor --fix');
616
+ return;
617
+ }
618
+ if (action === 'hook' || action === 'install-hook') {
619
+ const res = await installVersionGitHook(root, await globalSksCommand());
620
+ if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
621
+ console.log(res.installed ? `Version hook installed: ${res.hook_path}` : `Version hook skipped: ${res.reason}`);
622
+ return;
623
+ }
624
+ if (action === 'bump') {
625
+ const res = await runVersionPreCommit(root, { force: true });
626
+ if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
627
+ if (!res.ok) {
628
+ console.error(`Version bump failed: ${res.reason || 'unknown'}`);
629
+ process.exitCode = 2;
630
+ return;
631
+ }
632
+ console.log(res.changed ? `Project version bumped: ${res.previous_version} -> ${res.version}` : `Project version already advanced: ${res.version}`);
633
+ console.log(`Staged: ${res.staged_files?.join(', ') || 'none'}`);
634
+ return;
635
+ }
636
+ if (action === 'pre-commit') {
637
+ const res = await runVersionPreCommit(root);
638
+ if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
639
+ if (!res.ok) {
640
+ console.error(`SKS versioning failed: ${res.reason || 'unknown'}`);
641
+ process.exitCode = 2;
642
+ return;
643
+ }
644
+ if (res.skipped) return;
645
+ console.log(res.changed ? `SKS versioning: ${res.previous_version} -> ${res.version}` : `SKS versioning: ${res.version} already unique`);
646
+ return;
647
+ }
648
+ console.error('Usage: sks versioning status|bump|pre-commit [--json]');
649
+ process.exitCode = 1;
650
+ }
651
+
569
652
  async function reasoningCommand(args = []) {
570
653
  const prompt = promptOf(args);
571
654
  const route = routePrompt(prompt || '$SKS');
@@ -1008,14 +1091,34 @@ Print the LLM cleanup prompt:
1008
1091
  sks conflicts prompt
1009
1092
 
1010
1093
  Install behavior:
1011
- npm install/postinstall blocks when OMX, DCodex, or their global/repo-level traces are detected.
1012
- sks setup and sks doctor --fix also refuse to continue until a human approves cleanup.
1094
+ npm install/postinstall prints a clean setup-blocked notice when OMX, DCodex, or their global/repo-level traces are detected.
1095
+ npm can finish installing the package, but sks setup and sks doctor --fix refuse to continue until a human approves cleanup.
1013
1096
  If cleanup is denied, SKS cannot be installed in that environment.
1014
1097
 
1015
1098
  Cleanup operator:
1016
1099
  Use Codex App with GPT-5.5 high mode.
1017
1100
  Paste the prompt from sks conflicts prompt.
1018
1101
  The LLM must ask for explicit approval before deleting or moving conflicting harness artifacts.
1102
+ `,
1103
+ versioning: `Project Versioning
1104
+
1105
+ SKS installs a managed Git pre-commit hook during setup.
1106
+ Every commit in a package.json project gets a patch version bump in the same commit:
1107
+ sks versioning status
1108
+ sks versioning bump
1109
+ sks versioning hook
1110
+
1111
+ Commit behavior:
1112
+ package.json version is bumped before Git writes the commit.
1113
+ package-lock.json and npm-shrinkwrap.json are kept in sync when present.
1114
+ The hook stages those version files automatically.
1115
+
1116
+ Collision policy:
1117
+ SKS uses a lock in the Git common directory, so multiple workers or worktrees cannot reuse the same version.
1118
+ If another worker already used a version, the next commit bumps above the last seen version.
1119
+
1120
+ Emergency bypass:
1121
+ SKS_DISABLE_VERSIONING=1 git commit ...
1019
1122
  `,
1020
1123
  reasoning: `Reasoning Routing
1021
1124
 
@@ -1089,6 +1192,7 @@ async function setup(args) {
1089
1192
  const globalCommand = await globalSksCommand();
1090
1193
  const res = await initProject(root, { force: flag(args, '--force'), installScope, globalCommand, localOnly });
1091
1194
  const install = await installStatus(root, installScope, { globalCommand });
1195
+ const versioningInfo = await versioningStatus(root);
1092
1196
  const hooksPath = path.join(root, '.codex', 'hooks.json');
1093
1197
  const result = {
1094
1198
  root,
@@ -1103,6 +1207,7 @@ async function setup(args) {
1103
1207
  agents_rules: path.join(root, 'AGENTS.md')
1104
1208
  },
1105
1209
  created: res.created,
1210
+ versioning: versioningInfo,
1106
1211
  local_only: localOnly,
1107
1212
  next: ['sks context7 check', 'sks selftest --mock', 'sks doctor', 'sks commands']
1108
1213
  };
@@ -1111,6 +1216,7 @@ async function setup(args) {
1111
1216
  console.log(`Project: ${root}`);
1112
1217
  console.log(`Install: ${install.ok ? 'ok' : 'missing'} ${install.scope} (${install.command_prefix})`);
1113
1218
  console.log(`Hooks: ${path.relative(root, hooksPath)}`);
1219
+ console.log(`Version: ${versioningInfo.enabled ? (versioningInfo.hook_installed ? 'auto-bump enabled' : 'auto-bump hook missing') : 'not enabled'}${versioningInfo.package_version ? ` (${versioningInfo.package_version})` : ''}`);
1114
1220
  if (localOnly) console.log('Git: local-only (.git/info/exclude; user AGENTS preserved, SKS managed block refreshed)');
1115
1221
  console.log(`Codex App: .codex/config.toml, .codex/hooks.json, .agents/skills, .codex/agents, .codex/SNEAKOSCOPE.md`);
1116
1222
  console.log(`Prompt: skill-first pipeline, $DF fast design/content route, Context7 gate`);
@@ -1174,9 +1280,11 @@ async function doctor(args) {
1174
1280
  const context7Status = await checkContext7(root);
1175
1281
  const skillStatus = await checkRequiredSkills(root);
1176
1282
  const guardStatus = await harnessGuardStatus(root);
1283
+ const versioningInfo = await versioningStatus(root);
1177
1284
  const codexApp = {
1178
1285
  config: { ok: await exists(path.join(root, '.codex', 'config.toml')) },
1179
1286
  hooks: { ok: await exists(path.join(root, '.codex', 'hooks.json')) },
1287
+ versioning: versioningInfo,
1180
1288
  skills: skillStatus,
1181
1289
  agents: { ok: await exists(path.join(root, '.codex', 'agents')) },
1182
1290
  quick_reference: { ok: await exists(path.join(root, '.codex', 'SNEAKOSCOPE.md')) },
@@ -1196,6 +1304,7 @@ async function doctor(args) {
1196
1304
  sneakoscope: { ok: await exists(path.join(root, '.sneakoscope')) },
1197
1305
  context7: context7Status,
1198
1306
  harness_guard: guardStatus,
1307
+ versioning: versioningInfo,
1199
1308
  db_guard: { ok: dbPolicyExists && dbScan.ok, policy: dbPolicyExists ? await loadDbSafetyPolicy(root) : null, scan: dbScan },
1200
1309
  hooks: { ok: await exists(path.join(root, '.codex', 'hooks.json')) },
1201
1310
  skills: { ok: await exists(path.join(root, '.agents', 'skills')) },
@@ -1205,7 +1314,7 @@ async function doctor(args) {
1205
1314
  },
1206
1315
  package: { bytes: pkgBytes, human: formatBytes(pkgBytes) }, storage
1207
1316
  };
1208
- result.ready = !result.harness_conflicts.hard_block && nodeOk && Boolean(codex.bin) && install.ok && result.sneakoscope.ok && result.context7.ok && result.harness_guard.ok && result.db_guard.ok && result.codex_app.ok && result.skills.ok;
1317
+ result.ready = !result.harness_conflicts.hard_block && nodeOk && Boolean(codex.bin) && install.ok && result.sneakoscope.ok && result.context7.ok && result.harness_guard.ok && result.versioning.ok && result.db_guard.ok && result.codex_app.ok && result.skills.ok;
1209
1318
  if (result.harness_conflicts.hard_block) process.exitCode = 1;
1210
1319
  if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
1211
1320
  console.log('Sneakoscope Codex Doctor\n');
@@ -1220,6 +1329,7 @@ async function doctor(args) {
1220
1329
  console.log(`State: ${result.sneakoscope.ok ? 'ok' : 'missing .sneakoscope'}`);
1221
1330
  console.log(`Context7: ${result.context7.ok ? 'ok' : 'missing MCP config'} project=${result.context7.project.ok ? 'ok' : 'missing'} global=${result.context7.global.ok ? 'ok' : 'missing'}`);
1222
1331
  console.log(`Guard: ${result.harness_guard.ok ? 'ok' : 'blocked'}${result.harness_guard.source_exception ? ' source-exception' : ''}`);
1332
+ console.log(`Version: ${result.versioning.ok ? 'ok' : 'missing'}${result.versioning.enabled ? ` ${result.versioning.package_version || ''}` : ` ${result.versioning.reason || 'disabled'}`}`);
1223
1333
  console.log(`DB Guard: ${result.db_guard.ok ? 'ok' : 'blocked'} ${dbScan.findings?.length || 0} finding(s)`);
1224
1334
  console.log(`Hooks: ${result.hooks.ok ? 'ok' : 'missing .codex/hooks.json'}`);
1225
1335
  console.log(`Codex App: ${result.codex_app.ok ? 'ok' : 'missing app files'} .codex/config.toml .codex/hooks.json .agents/skills .codex/agents .codex/SNEAKOSCOPE.md`);
@@ -1233,6 +1343,7 @@ async function doctor(args) {
1233
1343
  if (result.harness_conflicts.hard_block) console.log(`\n${formatHarnessConflictReport(conflictScan)}`);
1234
1344
  if (!result.context7.ok) console.log('Context7 MCP missing. Run: sks context7 setup --scope project');
1235
1345
  if (!result.harness_guard.ok) console.log('Harness guard failed. Run: sks setup from a real terminal, then sks guard check.');
1346
+ if (!result.versioning.ok) console.log('Versioning hook missing. Run: sks versioning hook, or sks doctor --fix.');
1236
1347
  if (!result.skills.ok) console.log(`Missing skills: ${result.skills.missing.join(', ')}. Run: sks setup`);
1237
1348
  if (!result.ready && !flag(args, '--fix')) console.log('Run: sks doctor --fix');
1238
1349
  }
@@ -1615,7 +1726,11 @@ async function selftest() {
1615
1726
  const conflictScan = await scanHarnessConflicts(conflictTmp, { home: path.join(conflictTmp, 'home') });
1616
1727
  if (!conflictScan.hard_block || !formatHarnessConflictReport(conflictScan).includes('GPT-5.5')) throw new Error('selftest failed: OMX conflict did not block with cleanup prompt');
1617
1728
  const postinstallConflict = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
1618
- if (postinstallConflict.code === 0 || !String(postinstallConflict.stderr || postinstallConflict.stdout).includes('install blocked')) throw new Error('selftest failed: postinstall did not block OMX conflict');
1729
+ if (postinstallConflict.code !== 0) throw new Error('selftest failed: postinstall conflict notice should not make npm install fail');
1730
+ const postinstallConflictOutput = String(`${postinstallConflict.stdout}\n${postinstallConflict.stderr}`);
1731
+ if (!postinstallConflictOutput.includes('SKS setup is blocked') || postinstallConflictOutput.includes('Cleanup prompt:')) throw new Error('selftest failed: postinstall conflict notice did not stay informational');
1732
+ const postinstallConflictPrompt = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, input: 'y\n', env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_POSTINSTALL_PROMPT: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
1733
+ if (postinstallConflictPrompt.code !== 0 || !String(postinstallConflictPrompt.stdout || '').includes('Goal: completely remove the conflicting Codex harnesses')) throw new Error('selftest failed: interactive postinstall prompt did not print cleanup prompt');
1619
1734
  const guardBlocked = await checkHarnessModification(tmp, { tool_name: 'apply_patch', command: '*** Update File: .agents/skills/team/SKILL.md\n+tamper\n' });
1620
1735
  if (guardBlocked.action !== 'block') throw new Error('selftest failed: harness guard allowed skill tampering');
1621
1736
  const setupBlocked = await checkHarnessModification(tmp, { command: 'sks setup --force' });
@@ -1652,6 +1767,36 @@ async function selftest() {
1652
1767
  await initProject(projectScopeTmp, { installScope: 'project' });
1653
1768
  const projectHooks = await readJson(path.join(projectScopeTmp, '.codex', 'hooks.json'));
1654
1769
  if (projectHooks.hooks.PreToolUse[0].hooks[0].command !== 'node ./node_modules/sneakoscope/bin/sks.mjs hook pre-tool') throw new Error('selftest failed: project install hook command missing');
1770
+ const versionTmp = tmpdir();
1771
+ await runProcess('git', ['init'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1772
+ await runProcess('git', ['config', 'user.email', 'sks-selftest@example.invalid'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1773
+ await runProcess('git', ['config', 'user.name', 'SKS Selftest'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1774
+ await writeJsonAtomic(path.join(versionTmp, 'package.json'), { name: 'sks-version-selftest', version: '0.1.0' });
1775
+ await writeJsonAtomic(path.join(versionTmp, 'package-lock.json'), { name: 'sks-version-selftest', version: '0.1.0', lockfileVersion: 3, packages: { '': { name: 'sks-version-selftest', version: '0.1.0' } } });
1776
+ await runProcess('git', ['add', 'package.json', 'package-lock.json'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1777
+ await runProcess('git', ['commit', '--no-verify', '-m', 'initial'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1778
+ await writeTextAtomic(path.join(versionTmp, '.git', 'hooks', 'pre-commit'), '#!/bin/sh\nexit 0\n');
1779
+ await initProject(versionTmp, {});
1780
+ const versionStatus = await versioningStatus(versionTmp);
1781
+ if (!versionStatus.ok || !versionStatus.enabled || !versionStatus.hook_installed) throw new Error('selftest failed: versioning hook not installed');
1782
+ const versionHookText = await safeReadText(versionStatus.hook_path);
1783
+ if (!versionHookText.includes('versioning pre-commit')) throw new Error('selftest failed: versioning hook command missing');
1784
+ if (versionHookText.indexOf('versioning pre-commit') > versionHookText.indexOf('exit 0')) throw new Error('selftest failed: versioning hook was appended after an early exit');
1785
+ await writeTextAtomic(path.join(versionTmp, 'README.md'), 'version selftest\n');
1786
+ await runProcess('git', ['add', 'README.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1787
+ const firstVersionBump = await runVersionPreCommit(versionTmp);
1788
+ if (!firstVersionBump.ok || firstVersionBump.version !== '0.1.1' || !firstVersionBump.changed) throw new Error('selftest failed: first version bump did not advance patch version');
1789
+ const bumpedPackage = await readJson(path.join(versionTmp, 'package.json'));
1790
+ const bumpedLock = await readJson(path.join(versionTmp, 'package-lock.json'));
1791
+ if (bumpedPackage.version !== '0.1.1' || bumpedLock.version !== '0.1.1' || bumpedLock.packages[''].version !== '0.1.1') throw new Error('selftest failed: package lock versions not synced');
1792
+ const firstCached = await runProcess('git', ['diff', '--cached', '--name-only'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1793
+ if (!firstCached.stdout.includes('package.json') || !firstCached.stdout.includes('package-lock.json')) throw new Error('selftest failed: version files not staged');
1794
+ await runProcess('git', ['commit', '--no-verify', '-m', 'first versioned commit'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1795
+ await writeJsonAtomic(versionStatus.state_path, { schema_version: 1, last_version: '0.1.5', updated_at: nowIso(), pid: process.pid, changed: true });
1796
+ await writeTextAtomic(path.join(versionTmp, 'CHANGELOG.md'), 'collision selftest\n');
1797
+ await runProcess('git', ['add', 'CHANGELOG.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1798
+ const collisionBump = await runVersionPreCommit(versionTmp);
1799
+ if (!collisionBump.ok || collisionBump.version !== '0.1.6') throw new Error('selftest failed: version collision state did not bump above last seen version');
1655
1800
  const localOnlyTmp = tmpdir();
1656
1801
  await ensureDir(path.join(localOnlyTmp, '.git'));
1657
1802
  await writeTextAtomic(path.join(localOnlyTmp, 'AGENTS.md'), 'existing local rules\n');
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.6.24';
8
+ export const PACKAGE_VERSION = '0.6.25';
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
@@ -3,8 +3,9 @@ import fsp from 'node:fs/promises';
3
3
  import { ensureDir, readJson, readText, writeJsonAtomic, writeTextAtomic, mergeManagedBlock, nowIso, PACKAGE_VERSION, exists } from './fsx.mjs';
4
4
  import { DEFAULT_RETENTION_POLICY } from './retention.mjs';
5
5
  import { DEFAULT_DB_SAFETY_POLICY } from './db-safety.mjs';
6
- import { writeHarnessGuardPolicy } from './harness-guard.mjs';
6
+ import { isHarnessSourceProject, writeHarnessGuardPolicy } from './harness-guard.mjs';
7
7
  import { repairSksGeneratedArtifacts } from './harness-conflicts.mjs';
8
+ import { installVersionGitHook } from './version-manager.mjs';
8
9
  import { DOLLAR_COMMANDS, DOLLAR_COMMAND_ALIASES, DOLLAR_SKILL_NAMES, RECOMMENDED_MCP_SERVERS, RECOMMENDED_SKILLS, context7ConfigToml, triwikiContextTracking, triwikiContextTrackingText } from './routes.mjs';
9
10
 
10
11
  export function normalizeInstallScope(scope = 'global') {
@@ -99,6 +100,10 @@ Sneakoscope Codex keeps runtime state bounded. Do not write large raw logs into
99
100
 
100
101
  Before any substantive work, SKS hooks check whether the installed SKS package is behind the latest published package. If an update is available, ask the user to choose between updating now and skipping the update for this conversation only. If the user skips, continue the current conversation without asking again, but check again in the next conversation. If the user accepts, update SKS, rerun setup/doctor, then continue the original task.
101
102
 
103
+ ## Project Versioning
104
+
105
+ SKS manages the worker project's package version through a managed Git pre-commit hook. Every commit in a project with \`package.json\` gets a patch version bump in the same commit, with \`package-lock.json\` and \`npm-shrinkwrap.json\` kept in sync when present. The version guard uses a lock in the Git common directory so parallel workers or multiple worktrees do not reuse the same version. Check with \`sks versioning status\`; bypass only for exceptional maintenance with \`SKS_DISABLE_VERSIONING=1\`.
106
+
102
107
  ## Harness Self-Protection
103
108
 
104
109
  After setup, installed Sneakoscope harness control files are immutable to LLM tool edits. Do not edit \`.codex/hooks.json\`, \`.codex/config.toml\`, \`.codex/SNEAKOSCOPE.md\`, \`.agents/skills/\`, \`.codex/agents/\`, \`.sneakoscope/manifest.json\`, \`.sneakoscope/policy.json\`, \`.sneakoscope/db-safety.json\`, \`.sneakoscope/harness-guard.json\`, \`AGENTS.md\`, or \`node_modules/sneakoscope\` from the agent. SKS hooks block direct writes and SKS maintenance commands from LLM tool calls. The only automatic exception is the Sneakoscope engine source repository itself, detected by \`package.json\` name \`sneakoscope\` plus \`bin/sks.mjs\` and \`src/core/*\`.
@@ -253,7 +258,14 @@ export async function initProject(root, opts = {}) {
253
258
  git: {
254
259
  local_only: localOnly,
255
260
  exclude_path: localExclude?.path ? path.relative(root, localExclude.path) : null,
256
- excluded_patterns: localExclude?.patterns || []
261
+ excluded_patterns: localExclude?.patterns || [],
262
+ versioning: {
263
+ enabled: true,
264
+ hook: 'pre-commit',
265
+ bump: 'patch',
266
+ lock: 'git-common-dir/sks-version.lock',
267
+ state: 'git-common-dir/sks-version-state.json'
268
+ }
257
269
  },
258
270
  database_safety: 'destructive_db_operations_denied_always',
259
271
  gx_renderer: 'deterministic_svg_html'
@@ -279,7 +291,23 @@ export async function initProject(root, opts = {}) {
279
291
  ...(policy.git || {}),
280
292
  local_only: localOnly || Boolean(policy.git?.local_only),
281
293
  exclude_path: localExclude?.path ? path.relative(root, localExclude.path) : policy.git?.exclude_path || null,
282
- excluded_patterns: localExclude?.patterns || policy.git?.excluded_patterns || []
294
+ excluded_patterns: localExclude?.patterns || policy.git?.excluded_patterns || [],
295
+ versioning: {
296
+ ...(policy.git?.versioning || {}),
297
+ enabled: true,
298
+ hook: 'pre-commit',
299
+ bump: policy.git?.versioning?.bump || 'patch',
300
+ lock: 'git-common-dir/sks-version.lock',
301
+ state: 'git-common-dir/sks-version-state.json'
302
+ }
303
+ },
304
+ versioning: {
305
+ ...(policy.versioning || {}),
306
+ enabled: true,
307
+ bump: policy.versioning?.bump || 'patch',
308
+ trigger: 'git-pre-commit',
309
+ lock_scope: 'git-common-dir',
310
+ managed_files: ['package.json', 'package-lock.json', 'npm-shrinkwrap.json']
283
311
  },
284
312
  prompt_pipeline: {
285
313
  ...(policy.prompt_pipeline || {}),
@@ -329,7 +357,14 @@ export async function initProject(root, opts = {}) {
329
357
  git: {
330
358
  local_only: localOnly,
331
359
  exclude_path: localExclude?.path ? path.relative(root, localExclude.path) : null,
332
- excluded_patterns: localExclude?.patterns || []
360
+ excluded_patterns: localExclude?.patterns || [],
361
+ versioning: {
362
+ enabled: true,
363
+ hook: 'pre-commit',
364
+ bump: 'patch',
365
+ lock: 'git-common-dir/sks-version.lock',
366
+ state: 'git-common-dir/sks-version-state.json'
367
+ }
333
368
  },
334
369
  retention: DEFAULT_RETENTION_POLICY,
335
370
  update_check: {
@@ -338,6 +373,14 @@ export async function initProject(root, opts = {}) {
338
373
  prompt_user_before_work: true,
339
374
  skip_scope: 'conversation_only'
340
375
  },
376
+ versioning: {
377
+ enabled: true,
378
+ bump: 'patch',
379
+ trigger: 'git-pre-commit',
380
+ lock_scope: 'git-common-dir',
381
+ managed_files: ['package.json', 'package-lock.json', 'npm-shrinkwrap.json'],
382
+ collision_policy: 'lock_then_bump_above_last_seen_version'
383
+ },
341
384
  honest_mode: {
342
385
  required_before_final: true,
343
386
  verify_goal_evidence_tests_gaps: true
@@ -463,6 +506,12 @@ export async function initProject(root, opts = {}) {
463
506
  created.push('.codex/agents/*');
464
507
  await writeHarnessGuardPolicy(root);
465
508
  created.push('.sneakoscope/harness-guard.json');
509
+ const versionHookCommand = await isHarnessSourceProject(root).catch(() => false)
510
+ ? 'node ./bin/sks.mjs'
511
+ : hookCommandPrefix;
512
+ const versionHook = await installVersionGitHook(root, versionHookCommand);
513
+ if (versionHook.installed) created.push('.git/hooks/pre-commit SKS version guard');
514
+ else created.push(`version guard skipped (${versionHook.reason})`);
466
515
  return { created };
467
516
  }
468
517
 
@@ -1,4 +1,4 @@
1
- export const USAGE_TOPICS = 'install|setup|team|ralph|research|db|codex-app|df|dollar|context7|pipeline|reasoning|guard|conflicts|eval|gx|wiki';
1
+ export const USAGE_TOPICS = 'install|setup|team|ralph|research|db|codex-app|df|dollar|context7|pipeline|reasoning|guard|conflicts|versioning|eval|gx|wiki';
2
2
 
3
3
  export const RECOMMENDED_MCP_SERVERS = [
4
4
  {
@@ -194,6 +194,7 @@ export const COMMAND_CATALOG = [
194
194
  { name: 'pipeline', usage: 'sks pipeline status|resume [--json]', description: 'Inspect the active skill-first route and its completion gates.' },
195
195
  { name: 'guard', usage: 'sks guard check [--json]', description: 'Check SKS harness self-protection lock, fingerprints, and source-repo exception state.' },
196
196
  { name: 'conflicts', usage: 'sks conflicts check|prompt [--json]', description: 'Detect other Codex harnesses such as OMX/DCodex and print the GPT-5.5 high cleanup prompt.' },
197
+ { name: 'versioning', usage: 'sks versioning status|bump|pre-commit [--json]', description: 'Manage automatic project version bumps on every commit with a shared Git lock.' },
197
198
  { name: 'aliases', usage: 'sks aliases', description: 'Show command aliases and npm binary names.' },
198
199
  { name: 'setup', usage: 'sks setup [--install-scope global|project] [--local-only] [--force] [--json]', description: 'Initialize SKS state, Codex App files, hooks, skills, and rules.' },
199
200
  { name: 'fix-path', usage: 'sks fix-path [--install-scope global|project] [--json]', description: 'Refresh hook commands with the resolved SKS binary path.' },
@@ -0,0 +1,250 @@
1
+ import path from 'node:path';
2
+ import fsp from 'node:fs/promises';
3
+ import { ensureDir, exists, nowIso, readJson, runProcess, writeJsonAtomic, writeTextAtomic } from './fsx.mjs';
4
+
5
+ const VERSION_HOOK_MARKER = 'Sneakoscope Codex Version Guard';
6
+ const VERSION_STATE_FILE = 'sks-version-state.json';
7
+ const DEFAULT_BUMP = 'patch';
8
+
9
+ export async function installVersionGitHook(root, commandPrefix = 'sks') {
10
+ const git = await gitPaths(root);
11
+ if (!git.ok) return { ok: true, installed: false, reason: git.reason || 'not_git' };
12
+ const hookPath = git.hook_path;
13
+ const block = versionHookBlock(commandPrefix);
14
+ const current = await readFileMaybe(hookPath);
15
+ const next = mergeShellBlock(current, VERSION_HOOK_MARKER, block);
16
+ await ensureDir(path.dirname(hookPath));
17
+ await writeTextAtomic(hookPath, next);
18
+ await fsp.chmod(hookPath, 0o755).catch(() => {});
19
+ return { ok: true, installed: true, hook_path: hookPath };
20
+ }
21
+
22
+ export async function versioningStatus(root) {
23
+ const git = await gitPaths(root);
24
+ const packagePath = path.join(root, 'package.json');
25
+ const pkg = await readJson(packagePath, null);
26
+ const version = typeof pkg?.version === 'string' ? pkg.version : null;
27
+ if (!git.ok) return { ok: true, enabled: false, reason: git.reason || 'not_git', package_version: version };
28
+ const hookText = await readFileMaybe(git.hook_path);
29
+ const hookInstalled = hookText.includes(`BEGIN ${VERSION_HOOK_MARKER}`);
30
+ const policy = await versionPolicy(root);
31
+ const state = await readJson(path.join(git.common_dir, VERSION_STATE_FILE), {});
32
+ return {
33
+ ok: !policy.enabled || hookInstalled || !version,
34
+ enabled: Boolean(policy.enabled && version),
35
+ package_version: version,
36
+ bump: policy.bump,
37
+ hook_installed: hookInstalled,
38
+ hook_path: git.hook_path,
39
+ state_path: path.join(git.common_dir, VERSION_STATE_FILE),
40
+ last_version: state.last_version || null,
41
+ reason: version ? null : 'package_json_version_missing'
42
+ };
43
+ }
44
+
45
+ export async function runVersionPreCommit(root, opts = {}) {
46
+ if (process.env.SKS_DISABLE_VERSIONING === '1') return { ok: true, skipped: true, reason: 'SKS_DISABLE_VERSIONING=1' };
47
+ const policy = await versionPolicy(root);
48
+ if (!policy.enabled && !opts.force) return { ok: true, skipped: true, reason: 'disabled_by_policy' };
49
+ const pkgPath = path.join(root, 'package.json');
50
+ const pkg = await readJson(pkgPath, null);
51
+ if (!pkg?.version) return { ok: true, skipped: true, reason: 'package_json_version_missing' };
52
+ const git = await gitPaths(root);
53
+ if (!git.ok) return { ok: true, skipped: true, reason: git.reason || 'not_git' };
54
+ return withVersionLock(git.common_dir, async () => bumpProjectVersion(root, { ...opts, policy, git }));
55
+ }
56
+
57
+ export async function bumpProjectVersion(root, opts = {}) {
58
+ const policy = opts.policy || await versionPolicy(root);
59
+ const git = opts.git || await gitPaths(root);
60
+ const pkgPath = path.join(root, 'package.json');
61
+ const pkg = await readJson(pkgPath);
62
+ const current = parseSemver(pkg.version);
63
+ if (!current) return { ok: false, reason: `Unsupported package.json version: ${pkg.version}` };
64
+
65
+ const statePath = git.ok ? path.join(git.common_dir, VERSION_STATE_FILE) : null;
66
+ const state = statePath ? await readJson(statePath, {}) : {};
67
+ const headPkg = await gitJson(root, 'HEAD:package.json');
68
+ const headVersion = parseSemver(headPkg?.version);
69
+ const stateVersion = parseSemver(state.last_version);
70
+ const base = maxSemver([headVersion, stateVersion].filter(Boolean));
71
+ const manualAlreadyBumped = base && compareSemver(current, base) > 0;
72
+ const target = manualAlreadyBumped ? current : bumpSemver(base || current, policy.bump || DEFAULT_BUMP);
73
+ const changed = compareSemver(current, target) !== 0;
74
+
75
+ if (changed) {
76
+ pkg.version = formatSemver(target);
77
+ await writeJsonAtomic(pkgPath, pkg);
78
+ }
79
+ const synced = await syncPackageLockVersions(root, formatSemver(target));
80
+ const staged = await stageVersionFiles(root, [pkgPath, ...synced.files]);
81
+ if (!staged.ok) return { ok: false, reason: 'git_add_version_files_failed', stderr: staged.stderr };
82
+ if (statePath) {
83
+ await writeJsonAtomic(statePath, {
84
+ schema_version: 1,
85
+ last_version: formatSemver(target),
86
+ previous_version: pkg.version === formatSemver(target) && !changed ? formatSemver(current) : formatSemver(current),
87
+ updated_at: nowIso(),
88
+ pid: process.pid,
89
+ bump: policy.bump || DEFAULT_BUMP,
90
+ changed
91
+ });
92
+ }
93
+ await writeJsonAtomic(path.join(root, '.sneakoscope', 'version', 'last.json'), {
94
+ schema_version: 1,
95
+ version: formatSemver(target),
96
+ previous_version: formatSemver(current),
97
+ changed,
98
+ synced_files: synced.relative_files,
99
+ staged_files: staged.relative_files,
100
+ updated_at: nowIso()
101
+ }).catch(() => {});
102
+ return {
103
+ ok: true,
104
+ changed,
105
+ version: formatSemver(target),
106
+ previous_version: formatSemver(current),
107
+ synced_files: synced.relative_files,
108
+ staged_files: staged.relative_files,
109
+ lock_scope: git.common_dir
110
+ };
111
+ }
112
+
113
+ async function versionPolicy(root) {
114
+ const policy = await readJson(path.join(root, '.sneakoscope', 'policy.json'), {});
115
+ return {
116
+ enabled: policy.versioning?.enabled !== false,
117
+ bump: policy.versioning?.bump || DEFAULT_BUMP
118
+ };
119
+ }
120
+
121
+ async function gitPaths(root) {
122
+ const top = await git(root, ['rev-parse', '--show-toplevel']);
123
+ if (top.code !== 0) return { ok: false, reason: 'not_git' };
124
+ const common = await git(root, ['rev-parse', '--git-common-dir']);
125
+ const hook = await git(root, ['rev-parse', '--git-path', 'hooks/pre-commit']);
126
+ if (common.code !== 0 || hook.code !== 0) return { ok: false, reason: 'git_paths_unavailable' };
127
+ const topLevel = top.stdout.trim();
128
+ const commonDir = path.resolve(topLevel, common.stdout.trim());
129
+ const hookPath = path.resolve(topLevel, hook.stdout.trim());
130
+ return { ok: true, top_level: topLevel, common_dir: commonDir, hook_path: hookPath };
131
+ }
132
+
133
+ async function git(root, args, opts = {}) {
134
+ return runProcess('git', args, { cwd: root, timeoutMs: opts.timeoutMs || 15000, maxOutputBytes: opts.maxOutputBytes || 64 * 1024 });
135
+ }
136
+
137
+ async function gitJson(root, spec) {
138
+ const result = await git(root, ['show', spec], { maxOutputBytes: 256 * 1024 });
139
+ if (result.code !== 0) return null;
140
+ try { return JSON.parse(result.stdout); } catch { return null; }
141
+ }
142
+
143
+ async function withVersionLock(commonDir, fn) {
144
+ const lockDir = path.join(commonDir, 'sks-version.lock');
145
+ const started = Date.now();
146
+ let attempts = 0;
147
+ while (true) {
148
+ attempts += 1;
149
+ try {
150
+ await fsp.mkdir(lockDir);
151
+ await writeTextAtomic(path.join(lockDir, 'owner.json'), JSON.stringify({ pid: process.pid, started_at: nowIso() }, null, 2));
152
+ try {
153
+ const result = await fn();
154
+ return { ...result, lock_attempts: attempts };
155
+ } finally {
156
+ await fsp.rm(lockDir, { recursive: true, force: true }).catch(() => {});
157
+ }
158
+ } catch (err) {
159
+ if (err?.code !== 'EEXIST') throw err;
160
+ if (Date.now() - started > 15000) return { ok: false, reason: 'version_lock_timeout', lock_path: lockDir };
161
+ await sleep(150 + Math.min(750, attempts * 25));
162
+ }
163
+ }
164
+ }
165
+
166
+ function sleep(ms) {
167
+ return new Promise((resolve) => setTimeout(resolve, ms));
168
+ }
169
+
170
+ async function syncPackageLockVersions(root, version) {
171
+ const files = [];
172
+ for (const rel of ['package-lock.json', 'npm-shrinkwrap.json']) {
173
+ const file = path.join(root, rel);
174
+ const json = await readJson(file, null);
175
+ if (!json) continue;
176
+ let changed = false;
177
+ if (json.version && json.version !== version) { json.version = version; changed = true; }
178
+ if (json.packages?.['']?.version && json.packages[''].version !== version) {
179
+ json.packages[''].version = version;
180
+ changed = true;
181
+ }
182
+ if (changed) {
183
+ await writeJsonAtomic(file, json);
184
+ files.push(file);
185
+ }
186
+ }
187
+ return { files, relative_files: files.map((file) => path.relative(root, file)) };
188
+ }
189
+
190
+ async function stageVersionFiles(root, files) {
191
+ const existing = [];
192
+ for (const file of files) if (await exists(file)) existing.push(path.relative(root, file));
193
+ if (!existing.length) return { ok: true, relative_files: [] };
194
+ const result = await git(root, ['add', '--', ...existing]);
195
+ return { ok: result.code === 0, relative_files: existing, stderr: result.stderr };
196
+ }
197
+
198
+ function parseSemver(value) {
199
+ const match = String(value || '').trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
200
+ if (!match) return null;
201
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
202
+ }
203
+
204
+ function formatSemver(v) {
205
+ return `${v.major}.${v.minor}.${v.patch}`;
206
+ }
207
+
208
+ function compareSemver(a, b) {
209
+ for (const key of ['major', 'minor', 'patch']) {
210
+ if ((a?.[key] || 0) > (b?.[key] || 0)) return 1;
211
+ if ((a?.[key] || 0) < (b?.[key] || 0)) return -1;
212
+ }
213
+ return 0;
214
+ }
215
+
216
+ function maxSemver(items) {
217
+ return items.reduce((max, item) => (!max || compareSemver(item, max) > 0 ? item : max), null);
218
+ }
219
+
220
+ function bumpSemver(v, bump = DEFAULT_BUMP) {
221
+ if (bump === 'major') return { major: v.major + 1, minor: 0, patch: 0 };
222
+ if (bump === 'minor') return { major: v.major, minor: v.minor + 1, patch: 0 };
223
+ return { major: v.major, minor: v.minor, patch: v.patch + 1 };
224
+ }
225
+
226
+ function versionHookBlock(commandPrefix) {
227
+ return `# SKS keeps package versions unique across worker commits.\n${commandPrefix} versioning pre-commit\nstatus=$?\nif [ $status -ne 0 ]; then\n echo \"SKS versioning blocked commit. Run: sks versioning status\" >&2\n exit $status\nfi`;
228
+ }
229
+
230
+ function mergeShellBlock(current, marker, block) {
231
+ const begin = `# BEGIN ${marker}`;
232
+ const end = `# END ${marker}`;
233
+ const managed = `${begin}\n${block.trim()}\n${end}\n`;
234
+ if (!current.trim()) return `#!/bin/sh\n${managed}`;
235
+ const withShebang = current.startsWith('#!') ? current : `#!/bin/sh\n${current}`;
236
+ const beginIdx = withShebang.indexOf(begin);
237
+ const endIdx = withShebang.indexOf(end);
238
+ if (beginIdx >= 0 && endIdx >= beginIdx) {
239
+ return `${withShebang.slice(0, beginIdx)}${managed}${withShebang.slice(endIdx + end.length).replace(/^\n/, '')}`;
240
+ }
241
+ const lines = withShebang.split('\n');
242
+ if (lines[0]?.startsWith('#!')) {
243
+ return `${lines[0]}\n${managed}${lines.slice(1).join('\n').replace(/^\n/, '')}`.replace(/\s*$/, '\n');
244
+ }
245
+ return `${managed}${withShebang.replace(/^\n/, '').replace(/\s*$/, '\n')}`;
246
+ }
247
+
248
+ async function readFileMaybe(file) {
249
+ try { return await fsp.readFile(file, 'utf8'); } catch { return ''; }
250
+ }
@@ -1,83 +0,0 @@
1
- # Sneakoscope Codex performance and leak policy
2
-
3
- Sneakoscope Codex v0.6 is designed to keep runtime, package size, RAM, and storage bounded.
4
-
5
- ## Speed
6
-
7
- - `codex exec` output is streamed to files and only a bounded tail is retained in memory.
8
- - Ralph cycles run under a timeout and bounded max cycles.
9
- - TriWiki claim selection uses bounded top-K selection plus RGBA/trig wiki anchors instead of sorting unbounded context into prompts.
10
- - GX visual context renders deterministic SVG/HTML from JSON sources, avoiding external image-generation latency, cost, and nondeterminism. Rendered nodes expose the same RGBA wiki-coordinate anchors used by TriWiki.
11
- - `sks gc` runs after Ralph cycles by default.
12
-
13
- ## Evaluation metrics
14
-
15
- `sks eval run` creates a deterministic JSON report in `.sneakoscope/reports/` unless `--no-save` is used. The built-in scenario compares an uncompressed all-claims baseline with a TriWiki compressed context capsule.
16
-
17
- Tracked metrics:
18
-
19
- - `estimated_tokens`: deterministic chars/4 prompt-size estimate for local regression tracking
20
- - `token_savings_pct`: prompt-size reduction versus baseline
21
- - `accuracy_proxy`: evidence-weighted context-selection quality score
22
- - `required_recall`: required claim coverage
23
- - `relevance_precision`: selected required claims divided by selected claims
24
- - `support_ratio`: selected claims that are supported or weakly supported
25
- - `unsupported_critical_selected`: critical/high unsupported claims that survived compression
26
- - `context_build_ms_per_run`: local context construction runtime
27
- - `meaningful_improvement`: true only when token savings, accuracy delta, recall, unsupported-critical filtering, and runtime thresholds pass
28
-
29
- Default meaningful-improvement thresholds are intentionally explicit: at least 25% token savings, at least +0.03 accuracy-proxy delta, at least 0.95 required recall, zero unsupported critical claims selected, and candidate context construction under 25 ms per run. `sks eval compare --baseline old.json --candidate new.json` compares saved reports across implementations.
30
-
31
- The accuracy metric is not a live model task score. It is a deterministic proxy for whether the context handed to a model is smaller, better supported, and less contaminated by unsupported critical claims.
32
-
33
- ## LLM Wiki coordinate continuity
34
-
35
- TriWiki does not treat compression as permanent deletion. The visible context pack includes selected claim text plus a compact LLM Wiki coordinate index:
36
-
37
- ```text
38
- R channel -> domain angle
39
- G channel -> layer radius via sin()
40
- B channel -> phase angle
41
- A channel -> concentration/confidence
42
- ```
43
-
44
- Each anchor stores id, RGBA key, `[domain, layer, phase, concentration]`, source path, status/risk, and a text hash. This keeps non-selected claims hydratable across turns while keeping raw Q0 logs and large Q1 evidence out of the prompt until verification needs them.
45
-
46
- ## Package size
47
-
48
- - The npm package has zero runtime dependencies.
49
- - `@openai/codex` is no longer bundled. Users install Codex separately or set `SKS_CODEX_BIN`.
50
- - Optional Rust source is in `crates/` for the Git repo, but is excluded from the npm package by the `files` allowlist.
51
- - GX rendering uses only built-in Node.js APIs and ships as source in the npm package.
52
- - `npm run sizecheck` enforces package limits during `release:check`, `publish:dry`, and publish: `<=96 KiB` packed, `<=320 KiB` unpacked, `<=40` package files, and `<=256 KiB` per tracked file by default.
53
-
54
- ## Memory leaks
55
-
56
- - Child process stdout/stderr never accumulate unbounded strings.
57
- - Large outputs are written to log files and returned as tails.
58
- - Recursive file walking has file/depth caps.
59
- - No long-lived global caches are used.
60
-
61
- ## Storage leaks
62
-
63
- - `.sneakoscope/policy.json` controls retention.
64
- - Old missions, old Ralph cycle directories, arenas, temp files, and oversized JSONL logs are removed or rotated by `sks gc`.
65
- - `sks stats` reports package/state size.
66
-
67
- ## Rust decision
68
-
69
- Rust is useful for CPU-heavy long-running kernels, but not for the default npm package yet: native binaries increase package size and create OS/architecture install failure modes. Sneakoscope Codex therefore ships a zero-dependency Node runtime by default and includes an optional zero-dependency Rust helper source at `crates/sks-core` for future builds or users who want to compile locally.
70
-
71
- ## Database safety resource policy
72
-
73
- Sneakoscope Codex v0.3 adds a DB Safety Guard without adding runtime dependencies. It scans hook payloads and CLI commands with bounded string traversal and blocks high-risk database operations before Codex can execute them.
74
-
75
- Blocked classes include destructive SQL, direct remote SQL mutation, `supabase db reset`, `supabase db push`, migration history repair/squash, and project/branch destructive commands. The guard is intentionally conservative: when unsure, it blocks or warns rather than allowing a potentially destructive database operation.
76
-
77
- ## GX visual context policy
78
-
79
- Sneakoscope Codex v0.4 replaces model-rendered visual cartridges with deterministic code-rendered context sheets. `vgraph.json` and `beta.json` are the inputs, `render.svg` and `render.html` are reproducible outputs, and `drift.json` records whether the rendered source hash still matches the current graph.
80
-
81
- This keeps visual context cheap to regenerate, diffable in normal tooling, and safe to validate during npm packaging without network calls or model access.
82
-
83
- GX snapshots include `wiki_coordinates`, and `render.svg` nodes include `data-wiki-rgba` and `data-wiki-coord` attributes. This makes the visual context sheet and LLM Wiki pack share one deterministic coordinate system.
@@ -1,51 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title desc">
2
- <title id="title">Sneakoscope Codex logo</title>
3
- <desc id="desc">A brass magical danger-sensing lens with a red core, orbiting rings, and SKS initials.</desc>
4
- <defs>
5
- <radialGradient id="lens" cx="50%" cy="42%" r="58%">
6
- <stop offset="0%" stop-color="#fff4c2"/>
7
- <stop offset="42%" stop-color="#f0b54f"/>
8
- <stop offset="100%" stop-color="#7b241f"/>
9
- </radialGradient>
10
- <linearGradient id="brass" x1="92" x2="420" y1="80" y2="432">
11
- <stop offset="0%" stop-color="#ffe28a"/>
12
- <stop offset="46%" stop-color="#c9872e"/>
13
- <stop offset="100%" stop-color="#6c3218"/>
14
- </linearGradient>
15
- <linearGradient id="ink" x1="160" x2="352" y1="154" y2="358">
16
- <stop offset="0%" stop-color="#311018"/>
17
- <stop offset="100%" stop-color="#15070b"/>
18
- </linearGradient>
19
- <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
20
- <feDropShadow dx="0" dy="16" stdDeviation="18" flood-color="#12070a" flood-opacity=".28"/>
21
- </filter>
22
- </defs>
23
-
24
- <rect width="512" height="512" rx="108" fill="#16080d"/>
25
- <path fill="#241016" d="M64 346c61 66 122 96 193 96 72 0 138-34 197-101v106c0 21-17 38-38 38H102c-21 0-38-17-38-38V346Z" opacity=".65"/>
26
-
27
- <g filter="url(#shadow)">
28
- <path fill="none" stroke="url(#brass)" stroke-width="16" stroke-linecap="round" d="M99 256c41-82 95-123 158-123 62 0 115 41 156 123-41 82-94 123-156 123-63 0-117-41-158-123Z"/>
29
- <path fill="none" stroke="#f4c56a" stroke-width="5" stroke-linecap="round" d="M126 256c34-61 78-92 131-92 52 0 96 31 130 92-34 61-78 92-130 92-53 0-97-31-131-92Z" opacity=".86"/>
30
-
31
- <circle cx="256" cy="256" r="93" fill="url(#brass)"/>
32
- <circle cx="256" cy="256" r="74" fill="url(#ink)"/>
33
- <circle cx="256" cy="256" r="56" fill="url(#lens)"/>
34
- <circle cx="237" cy="236" r="16" fill="#fff8d8" opacity=".82"/>
35
- <circle cx="283" cy="282" r="9" fill="#5b1017" opacity=".75"/>
36
-
37
- <path fill="#fff0a4" d="M256 176l14 57 55-18-42 39 43 38-56-16-14 57-14-57-56 16 43-38-42-39 55 18 14-57Z"/>
38
- <circle cx="256" cy="256" r="17" fill="#7b0f17"/>
39
-
40
- <path fill="none" stroke="#f6d37c" stroke-width="11" stroke-linecap="round" d="M256 75v36M256 401v36M75 256h36M401 256h36"/>
41
- <path fill="none" stroke="#b86232" stroke-width="8" stroke-linecap="round" d="M133 132l26 26M353 354l26 26M379 132l-26 26M159 354l-26 26"/>
42
-
43
- <g fill="#fff0bc">
44
- <circle cx="127" cy="113" r="7"/>
45
- <circle cx="385" cy="113" r="7"/>
46
- <circle cx="127" cy="399" r="7"/>
47
- <circle cx="385" cy="399" r="7"/>
48
- </g>
49
- </g>
50
-
51
- </svg>