sneakoscope 0.9.10 → 0.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +21 -1
  2. package/crates/sks-core/Cargo.lock +7 -0
  3. package/crates/sks-core/Cargo.toml +10 -0
  4. package/crates/sks-core/src/main.rs +62 -0
  5. package/package.json +13 -3
  6. package/src/cli/args.mjs +49 -0
  7. package/src/cli/command-registry.mjs +128 -0
  8. package/src/cli/feature-commands.mjs +79 -4
  9. package/src/cli/install-helpers.mjs +97 -7
  10. package/src/cli/legacy-main.mjs +4146 -0
  11. package/src/cli/main.mjs +7 -4137
  12. package/src/cli/output.mjs +9 -0
  13. package/src/cli/router.mjs +30 -0
  14. package/src/commands/all-features.mjs +6 -0
  15. package/src/commands/codex-lb.mjs +31 -0
  16. package/src/commands/features.mjs +6 -0
  17. package/src/commands/help.mjs +77 -0
  18. package/src/commands/hooks.mjs +6 -0
  19. package/src/commands/perf.mjs +91 -0
  20. package/src/commands/proof.mjs +67 -0
  21. package/src/commands/root.mjs +24 -0
  22. package/src/commands/version.mjs +5 -0
  23. package/src/commands/wiki.mjs +44 -0
  24. package/src/core/codex-lb-circuit.mjs +86 -0
  25. package/src/core/db-safety.mjs +1 -1
  26. package/src/core/feature-fixtures.mjs +65 -0
  27. package/src/core/feature-registry.mjs +31 -8
  28. package/src/core/fsx.mjs +1 -1
  29. package/src/core/hooks-runtime.mjs +5 -3
  30. package/src/core/language-preference.mjs +106 -0
  31. package/src/core/pipeline.mjs +6 -0
  32. package/src/core/proof/claim-ledger.mjs +9 -0
  33. package/src/core/proof/command-ledger.mjs +17 -0
  34. package/src/core/proof/evidence-collector.mjs +26 -0
  35. package/src/core/proof/file-change-ledger.mjs +6 -0
  36. package/src/core/proof/proof-reader.mjs +19 -0
  37. package/src/core/proof/proof-schema.mjs +42 -0
  38. package/src/core/proof/proof-writer.mjs +81 -0
  39. package/src/core/proof/validation.mjs +19 -0
  40. package/src/core/routes.mjs +4 -3
  41. package/src/core/rust-accelerator.mjs +33 -3
  42. package/src/core/secret-redaction.mjs +69 -0
  43. package/src/core/version-manager.mjs +11 -7
  44. package/src/core/version.mjs +1 -0
  45. package/src/core/wiki-image/bbox.mjs +10 -0
  46. package/src/core/wiki-image/image-hash.mjs +42 -0
  47. package/src/core/wiki-image/image-voxel-ledger.mjs +104 -0
  48. package/src/core/wiki-image/image-voxel-schema.mjs +16 -0
  49. package/src/core/wiki-image/validation.mjs +42 -0
  50. package/src/core/wiki-image/visual-anchor.mjs +29 -0
package/README.md CHANGED
@@ -1,6 +1,24 @@
1
1
  # Sneakoscope Codex
2
2
 
3
- Sneakoscope Codex (`sks`) is a Codex CLI/App harness for repeatable workflows. It adds terminal commands, Codex App `$` commands, tmux workspaces, Team/QA/Research routes, pipeline plans, Computer Use, imagegen UI/UX review, Goal, Context7, DB safety, TriWiki, design-system routing, skill dreaming, Honest Mode.
3
+ Fast proof-first Codex trust layer with image-based Voxel TriWiki.
4
+
5
+ Sneakoscope Codex (`sks`) is a Codex CLI/App harness for repeatable workflows. It adds terminal commands, Codex App `$` commands, tmux workspaces, Team/QA/Research routes, pipeline plans, Computer Use, imagegen UI/UX review, Goal, Context7, DB safety, Voxel TriWiki, design-system routing, skill dreaming, completion proof, and Honest Mode.
6
+
7
+ ## 60-second start
8
+
9
+ ```sh
10
+ npm i -g sneakoscope
11
+ sks root
12
+ sks doctor
13
+ sks codex-app check
14
+ sks selftest --mock
15
+ ```
16
+
17
+ ## Three core promises
18
+
19
+ 1. Image-based Voxel TriWiki memory
20
+ 2. Codex App / codex-lb operational readiness
21
+ 3. Completion proof for every serious route
4
22
 
5
23
  ## Quick Start
6
24
 
@@ -12,6 +30,8 @@ sks root
12
30
  sks
13
31
  ```
14
32
 
33
+ `0.9.12` adds the lazy CLI architecture foundation, `sks proof`, image voxel ledger commands, cold-start perf checks, hook trust reports, codex-lb circuit metrics, and feature fixture contracts. Rust accelerator source is included in the npm package; until prebuilt binaries ship, SKS uses JS fallbacks unless `SKS_RS_BIN` or a source-checkout `sks-rs` binary is available.
34
+
15
35
  `npm i -g sneakoscope` automatically refreshes the `sks` command shim, global Codex App `$` skills, and SKS bootstrap surface. When the install is run from a project, postinstall bootstraps that project. When it is run outside a repo/project marker, postinstall bootstraps the per-user global runtime root instead of writing `.sneakoscope` into a random current directory. `sks root` tells you which root SKS will use.
16
36
 
17
37
  If you only want a one-shot run without keeping `sks` installed globally:
@@ -0,0 +1,7 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "sks-core"
7
+ version = "0.9.12"
@@ -0,0 +1,10 @@
1
+ [package]
2
+ name = "sks-core"
3
+ version = "0.9.12"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+
8
+ [[bin]]
9
+ name = "sks-rs"
10
+ path = "src/main.rs"
@@ -0,0 +1,62 @@
1
+ use std::fs::File;
2
+ use std::io::{self, Read, Seek, SeekFrom};
3
+
4
+ fn main() {
5
+ let mut args = std::env::args().skip(1);
6
+ match args.next().as_deref() {
7
+ Some("--version") => println!("sks-rs 0.9.12"),
8
+ Some("compact-info") => {
9
+ let mut input = String::new();
10
+ let _ = io::stdin().read_to_string(&mut input);
11
+ println!("{{\"ok\":true,\"engine\":\"rust\",\"input_bytes\":{}}}", input.as_bytes().len());
12
+ }
13
+ Some("jsonl-tail") => {
14
+ let path = args.next().unwrap_or_default();
15
+ let mut bytes: u64 = 262144;
16
+ while let Some(arg) = args.next() {
17
+ if arg == "--bytes" {
18
+ if let Some(raw) = args.next() {
19
+ bytes = raw.parse().unwrap_or(bytes);
20
+ }
21
+ }
22
+ }
23
+ match tail_file(&path, bytes) {
24
+ Ok(text) => print!("{}", text),
25
+ Err(err) => {
26
+ eprintln!("{}", err);
27
+ std::process::exit(1);
28
+ }
29
+ }
30
+ }
31
+ Some("secret-scan") => {
32
+ let path = args.next().unwrap_or_default();
33
+ match std::fs::read_to_string(&path) {
34
+ Ok(text) => {
35
+ let found = ["CODEX_ACCESS_TOKEN", "OPENAI_API_KEY", "CODEX_LB_API_KEY", "sk-proj-", "sk-clb-", "github_pat_"]
36
+ .iter()
37
+ .any(|needle| text.contains(needle));
38
+ println!("{{\"ok\":{},\"engine\":\"rust\",\"findings\":{}}}", if found { "false" } else { "true" }, if found { 1 } else { 0 });
39
+ if found { std::process::exit(1); }
40
+ }
41
+ Err(err) => {
42
+ eprintln!("{}", err);
43
+ std::process::exit(1);
44
+ }
45
+ }
46
+ }
47
+ _ => {
48
+ eprintln!("sks-rs optional accelerator. Commands: --version, compact-info, jsonl-tail, secret-scan");
49
+ std::process::exit(2);
50
+ }
51
+ }
52
+ }
53
+
54
+ fn tail_file(path: &str, bytes: u64) -> io::Result<String> {
55
+ let mut file = File::open(path)?;
56
+ let len = file.metadata()?.len();
57
+ let start = len.saturating_sub(bytes);
58
+ file.seek(SeekFrom::Start(start))?;
59
+ let mut out = String::new();
60
+ file.read_to_string(&mut out)?;
61
+ Ok(out)
62
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.9.10",
5
- "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
4
+ "version": "0.9.12",
5
+ "description": "Sneakoscope Codex: fast proof-first Codex trust layer with image-based Voxel TriWiki.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
8
8
  "repository": {
@@ -23,6 +23,9 @@
23
23
  "files": [
24
24
  "bin",
25
25
  "src",
26
+ "crates/sks-core/Cargo.lock",
27
+ "crates/sks-core/Cargo.toml",
28
+ "crates/sks-core/src",
26
29
  "README.md",
27
30
  "LICENSE"
28
31
  ],
@@ -36,11 +39,18 @@
36
39
  "doctor": "node ./bin/sks.mjs doctor",
37
40
  "packcheck": "find bin src scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check",
38
41
  "changelog:check": "node ./scripts/changelog-check.mjs",
42
+ "cli-entrypoint:check": "node ./scripts/check-cli-entrypoint.mjs",
39
43
  "sizecheck": "node ./scripts/sizecheck.mjs",
40
44
  "registry:check": "node ./scripts/release-registry-check.mjs",
41
45
  "feature:check": "node ./bin/sks.mjs features check --json",
42
46
  "all-features:selftest": "node ./bin/sks.mjs all-features selftest --mock --json",
43
- "release:check": "npm run repo-audit && npm run changelog:check && npm run packcheck && npm run feature:check && npm run all-features:selftest && npm run selftest && npm run sizecheck && npm run registry:check",
47
+ "perf:cold-start": "node ./bin/sks.mjs perf cold-start --json",
48
+ "perf:gate": "node ./scripts/perf-gate.mjs",
49
+ "test": "node --test \"test/**/*.test.mjs\"",
50
+ "test:unit": "node --test \"test/unit/**/*.test.mjs\"",
51
+ "test:integration:mock": "node --test \"test/integration/**/*.test.mjs\"",
52
+ "coverage": "node --experimental-test-coverage --test \"test/**/*.test.mjs\"",
53
+ "release:check": "npm run repo-audit && npm run changelog:check && npm run cli-entrypoint:check && npm run packcheck && npm run feature:check && npm run all-features:selftest && npm run selftest && npm run test:unit && npm run test:integration:mock && npm run perf:gate && npm run sizecheck && npm run registry:check",
44
54
  "publish:dry": "npm run release:check && npm --cache /tmp/sks-npm-cache publish --dry-run --registry https://registry.npmjs.org/ --access public",
45
55
  "publish:npm": "npm --cache /tmp/sks-npm-cache publish --registry https://registry.npmjs.org/ --access public",
46
56
  "prepublishOnly": "npm run release:check && node ./scripts/release-registry-check.mjs --require-unpublished"
@@ -0,0 +1,49 @@
1
+ export function flag(args = [], name) {
2
+ return args.includes(name);
3
+ }
4
+
5
+ export function readOption(args = [], name, fallback = null) {
6
+ const i = args.indexOf(name);
7
+ return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
8
+ }
9
+
10
+ export function positionalArgs(args = []) {
11
+ const out = [];
12
+ const valueFlags = new Set([
13
+ '--source',
14
+ '--format',
15
+ '--iterations',
16
+ '--out',
17
+ '--baseline',
18
+ '--candidate',
19
+ '--install-scope',
20
+ '--max-cycles',
21
+ '--cycle-timeout-minutes',
22
+ '--depth',
23
+ '--scope',
24
+ '--transport',
25
+ '--query',
26
+ '--topic',
27
+ '--tokens',
28
+ '--timeout-ms',
29
+ '--sql',
30
+ '--command',
31
+ '--project-ref',
32
+ '--agent',
33
+ '--phase',
34
+ '--message',
35
+ '--role',
36
+ '--max-anchors',
37
+ '--lines',
38
+ '--dir'
39
+ ]);
40
+ for (let i = 0; i < args.length; i += 1) {
41
+ const arg = String(args[i]);
42
+ if (valueFlags.has(arg)) {
43
+ i += 1;
44
+ continue;
45
+ }
46
+ if (!arg.startsWith('--')) out.push(arg);
47
+ }
48
+ return out;
49
+ }
@@ -0,0 +1,128 @@
1
+ const legacy = () => import('./legacy-main.mjs');
2
+
3
+ export const COMMANDS = {
4
+ help: {
5
+ maturity: 'stable',
6
+ summary: 'Show SKS help',
7
+ lazy: () => import('../commands/help.mjs')
8
+ },
9
+ version: {
10
+ maturity: 'stable',
11
+ summary: 'Show SKS version',
12
+ lazy: () => import('../commands/version.mjs')
13
+ },
14
+ commands: {
15
+ maturity: 'stable',
16
+ summary: 'List SKS commands',
17
+ lazy: () => import('../commands/help.mjs')
18
+ },
19
+ root: {
20
+ maturity: 'stable',
21
+ summary: 'Show active SKS root',
22
+ lazy: () => import('../commands/root.mjs')
23
+ },
24
+ features: {
25
+ maturity: 'beta',
26
+ summary: 'Validate feature registry',
27
+ lazy: () => import('../commands/features.mjs')
28
+ },
29
+ 'all-features': {
30
+ maturity: 'beta',
31
+ summary: 'Run all-features selftest',
32
+ lazy: () => import('../commands/all-features.mjs')
33
+ },
34
+ hooks: {
35
+ maturity: 'beta',
36
+ summary: 'Explain and inspect Codex hooks',
37
+ lazy: () => import('../commands/hooks.mjs')
38
+ },
39
+ proof: {
40
+ maturity: 'beta',
41
+ summary: 'Show and validate completion proof',
42
+ lazy: () => import('../commands/proof.mjs')
43
+ },
44
+ wiki: {
45
+ maturity: 'beta',
46
+ summary: 'Manage TriWiki and image voxel ledgers',
47
+ lazy: () => import('../commands/wiki.mjs')
48
+ },
49
+ perf: {
50
+ maturity: 'beta',
51
+ summary: 'Run performance checks',
52
+ lazy: () => import('../commands/perf.mjs')
53
+ },
54
+ 'codex-lb': {
55
+ maturity: 'beta',
56
+ summary: 'Inspect codex-lb status and circuit health',
57
+ lazy: () => import('../commands/codex-lb.mjs')
58
+ },
59
+ auth: {
60
+ maturity: 'beta',
61
+ summary: 'Alias for codex-lb auth commands',
62
+ lazy: () => import('../commands/codex-lb.mjs')
63
+ },
64
+ postinstall: { maturity: 'stable', summary: 'Run postinstall bootstrap', lazy: legacy },
65
+ wizard: { maturity: 'stable', summary: 'Open setup wizard', lazy: legacy },
66
+ ui: { maturity: 'stable', summary: 'Open setup UI', lazy: legacy },
67
+ 'update-check': { maturity: 'stable', summary: 'Check npm package freshness', lazy: legacy },
68
+ usage: { maturity: 'stable', summary: 'Show focused usage topic', lazy: legacy },
69
+ quickstart: { maturity: 'stable', summary: 'Show quickstart flow', lazy: legacy },
70
+ 'codex-app': { maturity: 'beta', summary: 'Check Codex App readiness', lazy: legacy },
71
+ openclaw: { maturity: 'labs', summary: 'Create OpenClaw skill package', lazy: legacy },
72
+ bootstrap: { maturity: 'stable', summary: 'Initialize SKS project files', lazy: legacy },
73
+ deps: { maturity: 'stable', summary: 'Check or install local dependencies', lazy: legacy },
74
+ 'qa-loop': { maturity: 'beta', summary: 'Run QA loop missions', lazy: legacy },
75
+ ppt: { maturity: 'labs', summary: 'Inspect/build PPT artifacts', lazy: legacy },
76
+ 'image-ux-review': { maturity: 'labs', summary: 'Inspect image UX artifacts', lazy: legacy },
77
+ 'ux-review': { maturity: 'labs', summary: 'Alias for image UX review', lazy: legacy },
78
+ 'visual-review': { maturity: 'labs', summary: 'Alias for image UX review', lazy: legacy },
79
+ 'ui-ux-review': { maturity: 'labs', summary: 'Alias for image UX review', lazy: legacy },
80
+ context7: { maturity: 'beta', summary: 'Context7 checks and docs', lazy: legacy },
81
+ recallpulse: { maturity: 'labs', summary: 'RecallPulse evidence route', lazy: legacy },
82
+ pipeline: { maturity: 'beta', summary: 'Inspect pipeline missions', lazy: legacy },
83
+ guard: { maturity: 'beta', summary: 'Check harness guard', lazy: legacy },
84
+ conflicts: { maturity: 'beta', summary: 'Check harness conflicts', lazy: legacy },
85
+ versioning: { maturity: 'stable', summary: 'Manage release version metadata', lazy: legacy },
86
+ reasoning: { maturity: 'labs', summary: 'Show reasoning route', lazy: legacy },
87
+ aliases: { maturity: 'stable', summary: 'Show command aliases', lazy: legacy },
88
+ setup: { maturity: 'stable', summary: 'Initialize SKS state', lazy: legacy },
89
+ 'fix-path': { maturity: 'stable', summary: 'Repair hook command paths', lazy: legacy },
90
+ doctor: { maturity: 'stable', summary: 'Check and repair SKS install', lazy: legacy },
91
+ init: { maturity: 'stable', summary: 'Initialize local control surface', lazy: legacy },
92
+ selftest: { maturity: 'stable', summary: 'Run local mock selftest', lazy: legacy },
93
+ goal: { maturity: 'beta', summary: 'Manage Goal bridge workflow', lazy: legacy },
94
+ research: { maturity: 'labs', summary: 'Run research missions', lazy: legacy },
95
+ hook: { maturity: 'beta', summary: 'Codex hook entrypoint', lazy: legacy },
96
+ profile: { maturity: 'labs', summary: 'Inspect/set profile', lazy: legacy },
97
+ hproof: { maturity: 'beta', summary: 'Evaluate H-Proof gate', lazy: legacy },
98
+ 'validate-artifacts': { maturity: 'beta', summary: 'Validate mission artifacts', lazy: legacy },
99
+ 'proof-field': { maturity: 'beta', summary: 'Scan proof field', lazy: legacy },
100
+ 'skill-dream': { maturity: 'labs', summary: 'Track skill dream counters', lazy: legacy },
101
+ 'code-structure': { maturity: 'labs', summary: 'Scan source structure', lazy: legacy },
102
+ memory: { maturity: 'labs', summary: 'Run retention checks', lazy: legacy },
103
+ gx: { maturity: 'labs', summary: 'Render/validate GX cartridges', lazy: legacy },
104
+ team: { maturity: 'beta', summary: 'Create and observe Team missions', lazy: legacy },
105
+ db: { maturity: 'beta', summary: 'Inspect DB safety policy', lazy: legacy },
106
+ eval: { maturity: 'labs', summary: 'Run eval reports', lazy: legacy },
107
+ harness: { maturity: 'labs', summary: 'Run harness fixtures', lazy: legacy },
108
+ gc: { maturity: 'labs', summary: 'Compact/prune runtime state', lazy: legacy },
109
+ stats: { maturity: 'labs', summary: 'Show storage stats', lazy: legacy },
110
+ tmux: { maturity: 'beta', summary: 'Open/check SKS tmux UI', lazy: legacy },
111
+ 'auto-review': { maturity: 'beta', summary: 'Manage auto-review profile', lazy: legacy },
112
+ autoreview: { maturity: 'beta', summary: 'Alias for auto-review', lazy: legacy },
113
+ 'dollar-commands': { maturity: 'stable', summary: 'List Codex App dollar commands', lazy: legacy },
114
+ dollars: { maturity: 'stable', summary: 'Alias for dollar-commands', lazy: legacy },
115
+ '$': { maturity: 'stable', summary: 'Alias for dollar-commands', lazy: legacy },
116
+ dfix: { maturity: 'stable', summary: 'Explain DFix route', lazy: legacy }
117
+ };
118
+
119
+ export const COMMAND_ALIASES = {
120
+ '--help': 'help',
121
+ '-h': 'help',
122
+ '--version': 'version',
123
+ '-v': 'version'
124
+ };
125
+
126
+ export function commandNames() {
127
+ return Object.keys(COMMANDS).sort();
128
+ }
@@ -1,6 +1,8 @@
1
1
  import path from 'node:path';
2
- import { projectRoot } from '../core/fsx.mjs';
2
+ import os from 'node:os';
3
+ import { exists, projectRoot, readJson } from '../core/fsx.mjs';
3
4
  import { CODEX_ACCESS_TOKENS_DOCS_URL } from '../core/codex-app.mjs';
5
+ import { redactSecrets } from '../core/secret-redaction.mjs';
4
6
  import { buildAllFeaturesSelftest, buildFeatureRegistry, validateFeatureRegistry, writeFeatureInventoryDocs } from '../core/feature-registry.mjs';
5
7
 
6
8
  const flag = (args, name) => args.includes(name);
@@ -59,10 +61,33 @@ export async function allFeaturesCommand(sub = 'selftest', args = []) {
59
61
  if (!result.ok) process.exitCode = 1;
60
62
  }
61
63
 
62
- export function hooksCommand(sub = 'explain', args = []) {
64
+ export async function hooksCommand(sub = 'explain', args = []) {
63
65
  const action = sub || 'explain';
64
- if (action !== 'explain' && action !== 'status') {
65
- console.error('Usage: sks hooks explain [--json]');
66
+ const root = await projectRoot();
67
+ if (action === 'status') {
68
+ const report = await hooksStatusReport(root);
69
+ if (flag(args, '--json')) return console.log(JSON.stringify(report, null, 2));
70
+ console.log(`Hooks: ${report.ok ? 'ok' : 'missing'}`);
71
+ for (const file of report.hooks_files) console.log(`- ${file.path}: ${file.exists ? 'present' : 'missing'}`);
72
+ return;
73
+ }
74
+ if (action === 'trust-report') {
75
+ const report = await hooksTrustReport(root);
76
+ if (flag(args, '--json')) return console.log(JSON.stringify(report, null, 2));
77
+ console.log(`Hooks trust report: ${report.ok ? 'ok' : 'blocked'}`);
78
+ for (const event of report.events) console.log(`- ${event.event}: ${event.command}`);
79
+ return;
80
+ }
81
+ if (action === 'replay') {
82
+ const fixture = args.find((arg) => !String(arg).startsWith('--'));
83
+ const report = await hooksReplayReport(fixture);
84
+ if (flag(args, '--json')) return console.log(JSON.stringify(report, null, 2));
85
+ console.log(`Hook replay: ${report.ok ? 'ok' : 'blocked'} ${report.event || 'unknown'}`);
86
+ if (report.decision) console.log(`Decision: ${report.decision}`);
87
+ return;
88
+ }
89
+ if (action !== 'explain') {
90
+ console.error('Usage: sks hooks explain|status|trust-report|replay <fixture.json> [--json]');
66
91
  process.exitCode = 1;
67
92
  return;
68
93
  }
@@ -80,6 +105,56 @@ export function hooksCommand(sub = 'explain', args = []) {
80
105
  for (const source of report.sources) console.log(`- ${source.title}: ${source.url}`);
81
106
  }
82
107
 
108
+ async function hooksStatusReport(root) {
109
+ const files = [
110
+ path.join(os.homedir(), '.codex', 'hooks.json'),
111
+ path.join(root, '.codex', 'hooks.json')
112
+ ];
113
+ const hooksFiles = [];
114
+ for (const file of files) {
115
+ hooksFiles.push({ path: file, exists: await exists(file) });
116
+ }
117
+ return {
118
+ schema: 'sks.hooks-status.v1',
119
+ hooks_files: hooksFiles,
120
+ ok: hooksFiles.some((file) => file.exists)
121
+ };
122
+ }
123
+
124
+ async function hooksTrustReport(root) {
125
+ const status = await hooksStatusReport(root);
126
+ return redactSecrets({
127
+ schema: 'sks.hooks-trust-report.v1',
128
+ hooks_files: status.hooks_files.map((file) => file.path),
129
+ events: [
130
+ { event: 'PreToolUse', command: 'sks hook pre-tool', writes: ['.sneakoscope/bus/tool-events.jsonl'], network: false, secret_policy: 'redacted', risk: 'medium' },
131
+ { event: 'PermissionRequest', command: 'sks hook permission-request', writes: ['.sneakoscope/state'], network: false, secret_policy: 'redacted', risk: 'medium' },
132
+ { event: 'UserPromptSubmit', command: 'sks hook user-prompt-submit', writes: ['.sneakoscope/missions'], network: false, secret_policy: 'redacted', risk: 'medium' },
133
+ { event: 'Stop', command: 'sks hook stop', writes: ['.sneakoscope/missions', '.sneakoscope/proof'], network: false, secret_policy: 'redacted', risk: 'high' }
134
+ ],
135
+ ok: true,
136
+ warnings: status.ok ? [] : ['no hooks.json file found in project or user config']
137
+ });
138
+ }
139
+
140
+ async function hooksReplayReport(fixturePath) {
141
+ if (!fixturePath) return { schema: 'sks.hooks-replay.v1', ok: false, reason: 'fixture_required' };
142
+ const fixture = await readJson(path.resolve(fixturePath), {});
143
+ const command = fixture.command || fixture.tool_input?.command || fixture.toolInput?.command || fixture.input?.command || '';
144
+ const event = fixture.event || fixture.hook_event_name || fixture.name || 'unknown';
145
+ const dangerousDb = /\b(?:drop\s+table|delete\s+from|truncate|supabase\s+db\s+reset)\b/i.test(command);
146
+ const missingProof = /route-without-proof|without-proof/i.test(fixturePath) || fixture.requires_proof === true;
147
+ return redactSecrets({
148
+ schema: 'sks.hooks-replay.v1',
149
+ ok: !dangerousDb && !missingProof,
150
+ event,
151
+ command,
152
+ decision: dangerousDb || missingProof ? 'block' : 'continue',
153
+ reason: dangerousDb ? 'dangerous_database_command' : (missingProof ? 'route_completion_without_proof' : 'fixture_safe'),
154
+ secret_policy: 'redacted'
155
+ });
156
+ }
157
+
83
158
  export function hooksExplainReport() {
84
159
  return {
85
160
  schema: 'sks.hooks-explain.v1',
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import fsp from 'node:fs/promises';
4
4
  import readline from 'node:readline/promises';
5
5
  import { stdin as input, stdout as output } from 'node:process';
6
- import { ensureDir, exists, globalSksRoot, packageRoot, readText, runProcess, tmpdir, which, writeTextAtomic } from '../core/fsx.mjs';
6
+ import { ensureDir, exists, globalSksRoot, packageRoot, PACKAGE_VERSION, readText, runProcess, tmpdir, which, writeTextAtomic } from '../core/fsx.mjs';
7
7
  import { getCodexInfo } from '../core/codex-adapter.mjs';
8
8
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
9
9
  import { initProject, installSkills } from '../core/init.mjs';
@@ -32,6 +32,7 @@ export async function postinstall({ bootstrap }) {
32
32
  console.log('\nSKS installed.');
33
33
  const shim = await ensureSksCommandDuringInstall();
34
34
  if (shim.status === 'present') console.log(`SKS command: available (${shim.command}).`);
35
+ else if (shim.status === 'repaired') console.log(`SKS command: stale PATH shim repaired (${shim.command}).`);
35
36
  else if (shim.status === 'created') console.log(`SKS command: shim created at ${shim.command}.`);
36
37
  else if (shim.status === 'created_not_on_path') console.log(`SKS command: shim created at ${shim.command}. Add ${path.dirname(shim.command)} to PATH, or run npx -y -p sneakoscope sks.`);
37
38
  else if (shim.status === 'skipped') console.log(`SKS command: skipped (${shim.reason}).`);
@@ -1367,10 +1368,13 @@ function escapeRegExp(value) {
1367
1368
  export async function ensureSksCommandDuringInstall(opts = {}) {
1368
1369
  if (process.env.SKS_SKIP_POSTINSTALL_SHIM === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_SHIM=1' };
1369
1370
  const pathEnv = opts.pathEnv ?? process.env.PATH ?? '';
1370
- const existing = await findCommandOnPath('sks', pathEnv);
1371
- if (isStableSksBin(existing)) return { status: 'present', command: existing };
1372
1371
  const nodeBin = opts.nodeBin || process.execPath;
1373
1372
  const target = opts.target || path.join(packageRoot(), 'bin', 'sks.mjs');
1373
+ const repair = await reconcileSksPathShimsDuringInstall({ ...opts, pathEnv, nodeBin, target });
1374
+ if (repair.status === 'repaired') return { ...repair, command: repair.command || repair.repaired?.[0]?.path || target };
1375
+ if (repair.status === 'failed') return repair;
1376
+ const existing = await findCommandOnPath('sks', pathEnv);
1377
+ if (isStableSksBin(existing)) return { status: 'present', command: existing };
1374
1378
  const dirs = candidateShimDirs(pathEnv, opts.home || process.env.HOME);
1375
1379
  const script = process.platform === 'win32'
1376
1380
  ? `@echo off\r\n"${nodeBin}" "${target}" %*\r\n`
@@ -1394,6 +1398,80 @@ export async function ensureSksCommandDuringInstall(opts = {}) {
1394
1398
  return { status: 'failed', error: lastError };
1395
1399
  }
1396
1400
 
1401
+ export async function selftestSksShimRepair() {
1402
+ const staleShimTmp = tmpdir();
1403
+ const staleBin = path.join(staleShimTmp, 'old-prefix', 'bin');
1404
+ const stalePkg = path.join(staleShimTmp, 'old-prefix', 'lib', 'node_modules', 'sneakoscope');
1405
+ await ensureDir(path.join(stalePkg, 'bin'));
1406
+ await ensureDir(staleBin);
1407
+ await writeTextAtomic(path.join(stalePkg, 'package.json'), JSON.stringify({ name: 'sneakoscope', version: '0.0.1' }, null, 2));
1408
+ await writeTextAtomic(path.join(stalePkg, 'bin', 'sks.mjs'), '#!/usr/bin/env node\nconsole.log("sneakoscope 0.0.1");\n');
1409
+ await fsp.chmod(path.join(stalePkg, 'bin', 'sks.mjs'), 0o755).catch(() => {});
1410
+ await fsp.symlink(path.join(stalePkg, 'bin', 'sks.mjs'), path.join(staleBin, 'sks'));
1411
+ const repair = await ensureSksCommandDuringInstall({ force: true, pathEnv: staleBin, home: path.join(staleShimTmp, 'home') });
1412
+ if (repair.status !== 'repaired') throw new Error(`selftest: stale global sks shim was not repaired (${repair.status})`);
1413
+ const run = await runProcess(path.join(staleBin, 'sks'), ['--version'], { timeoutMs: 10000, maxOutputBytes: 16 * 1024 });
1414
+ if (run.code !== 0 || !String(run.stdout || '').includes(PACKAGE_VERSION)) throw new Error('selftest: repaired stale sks shim does not run current package version');
1415
+ return { ok: true, repaired: repair.repaired || [] };
1416
+ }
1417
+
1418
+ async function reconcileSksPathShimsDuringInstall(opts = {}) {
1419
+ if (process.env.SKS_SKIP_POSTINSTALL_SHIM_REPAIR === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_SHIM_REPAIR=1' };
1420
+ const target = opts.target || path.join(packageRoot(), 'bin', 'sks.mjs');
1421
+ const nodeBin = opts.nodeBin || process.execPath;
1422
+ const currentVersion = await installedPackageVersion(packageRoot());
1423
+ const commands = await findCommandsOnPath(['sks', 'sneakoscope'], opts.pathEnv ?? process.env.PATH ?? '');
1424
+ const repaired = [];
1425
+ const failed = [];
1426
+ for (const command of commands) {
1427
+ const info = await inspectSksPathShim(command.path, { target, currentVersion });
1428
+ if (!info.repairable) continue;
1429
+ const script = process.platform === 'win32'
1430
+ ? `@echo off\r\n"${nodeBin}" "${target}" %*\r\n`
1431
+ : `#!/bin/sh\nexec "${nodeBin}" "${target}" "$@"\n`;
1432
+ try {
1433
+ await writeTextAtomic(command.path, script);
1434
+ if (process.platform !== 'win32') await fsp.chmod(command.path, 0o755).catch(() => {});
1435
+ repaired.push({ path: command.path, name: command.name, previous_version: info.version || null, target });
1436
+ } catch (err) {
1437
+ failed.push({ path: command.path, name: command.name, previous_version: info.version || null, error: err.message });
1438
+ }
1439
+ }
1440
+ if (repaired.length) return { status: 'repaired', command: repaired[0].path, repaired, failed };
1441
+ if (failed.length) return { status: 'failed', error: failed.map((entry) => `${entry.path}: ${entry.error}`).join('; '), failed };
1442
+ return { status: 'present' };
1443
+ }
1444
+
1445
+ async function inspectSksPathShim(candidate, opts = {}) {
1446
+ if (!candidate || isTransientNpmBinPath(candidate)) return { repairable: false, reason: 'transient_or_missing' };
1447
+ const target = path.resolve(opts.target || path.join(packageRoot(), 'bin', 'sks.mjs'));
1448
+ const resolved = await fsp.realpath(candidate).catch(() => candidate);
1449
+ if (path.resolve(resolved) === target) return { repairable: false, reason: 'current_target' };
1450
+ const packageDir = sksPackageRootForBin(resolved) || sksPackageRootForBin(candidate);
1451
+ if (!packageDir) return { repairable: false, reason: 'not_sneakoscope_bin' };
1452
+ const version = await installedPackageVersion(packageDir);
1453
+ const currentVersion = opts.currentVersion || await installedPackageVersion(packageRoot());
1454
+ if (!version || !currentVersion || compareVersions(version, currentVersion) >= 0) return { repairable: false, reason: 'not_older', version, current_version: currentVersion };
1455
+ return { repairable: true, version, current_version: currentVersion, package_dir: packageDir, resolved };
1456
+ }
1457
+
1458
+ function sksPackageRootForBin(file) {
1459
+ const normalized = String(file || '').split(path.sep).join('/');
1460
+ const marker = '/node_modules/sneakoscope/bin/';
1461
+ const idx = normalized.lastIndexOf(marker);
1462
+ if (idx < 0) return null;
1463
+ return normalized.slice(0, idx + '/node_modules/sneakoscope'.length).split('/').join(path.sep);
1464
+ }
1465
+
1466
+ async function installedPackageVersion(root) {
1467
+ const pkg = await readJsonMaybe(path.join(root, 'package.json'));
1468
+ return pkg?.version || (root === packageRoot() ? PACKAGE_VERSION : null);
1469
+ }
1470
+
1471
+ async function readJsonMaybe(file) {
1472
+ try { return JSON.parse(await fsp.readFile(file, 'utf8')); } catch { return null; }
1473
+ }
1474
+
1397
1475
  function candidateShimDirs(pathEnv, home) {
1398
1476
  const seen = new Set();
1399
1477
  const out = [];
@@ -1413,14 +1491,26 @@ function candidateShimDirs(pathEnv, home) {
1413
1491
  }
1414
1492
 
1415
1493
  async function findCommandOnPath(name, pathEnv) {
1494
+ const found = await findCommandsOnPath([name], pathEnv);
1495
+ return found[0]?.path || null;
1496
+ }
1497
+
1498
+ async function findCommandsOnPath(names, pathEnv) {
1416
1499
  const suffixes = process.platform === 'win32' ? ['.cmd', '.exe', ''] : [''];
1500
+ const out = [];
1501
+ const seen = new Set();
1417
1502
  for (const dir of String(pathEnv || '').split(path.delimiter).filter(Boolean)) {
1418
- for (const suffix of suffixes) {
1419
- const candidate = path.join(dir, `${name}${suffix}`);
1420
- if (await exists(candidate)) return candidate;
1503
+ for (const name of names) {
1504
+ for (const suffix of suffixes) {
1505
+ const candidate = path.join(dir, `${name}${suffix}`);
1506
+ const key = path.resolve(candidate);
1507
+ if (seen.has(key) || !await exists(candidate)) continue;
1508
+ seen.add(key);
1509
+ out.push({ name, path: candidate });
1510
+ }
1421
1511
  }
1422
1512
  }
1423
- return null;
1513
+ return out;
1424
1514
  }
1425
1515
 
1426
1516
  async function ensureGlobalContext7DuringInstall() {