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 +38 -78
- package/package.json +1 -2
- package/src/cli/main.mjs +153 -8
- package/src/core/fsx.mjs +1 -1
- package/src/core/init.mjs +53 -4
- package/src/core/routes.mjs +2 -1
- package/src/core/version-manager.mjs +250 -0
- package/docs/PERFORMANCE.md +0 -83
- package/docs/assets/sneakoscope-codex-logo.svg +0 -51
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
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
|
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
|
|
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 [
|
|
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`, `
|
|
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 `<=
|
|
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.
|
|
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
|
-
|
|
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
|
|
1012
|
-
sks setup and sks doctor --fix
|
|
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
|
|
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.
|
|
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
|
|
package/src/core/routes.mjs
CHANGED
|
@@ -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
|
+
}
|
package/docs/PERFORMANCE.md
DELETED
|
@@ -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>
|