sneakoscope 0.9.12 → 0.9.13
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 +8 -2
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +142 -2
- package/package.json +4 -2
- package/src/cli/command-registry.mjs +3 -3
- package/src/cli/feature-commands.mjs +42 -11
- package/src/cli/install-helpers.mjs +14 -7
- package/src/cli/legacy-main.mjs +5 -4
- package/src/commands/codex-app.mjs +30 -0
- package/src/commands/codex-lb.mjs +18 -2
- package/src/commands/db.mjs +6 -0
- package/src/commands/doctor.mjs +46 -0
- package/src/commands/proof.mjs +38 -2
- package/src/commands/wiki.mjs +53 -2
- package/src/core/codex-lb-circuit.mjs +45 -1
- package/src/core/db-safety.mjs +17 -2
- package/src/core/feature-fixtures.mjs +39 -1
- package/src/core/feature-registry.mjs +87 -4
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +12 -3
- package/src/core/pipeline.mjs +18 -0
- package/src/core/proof/evidence-collector.mjs +7 -0
- package/src/core/proof/proof-reader.mjs +11 -0
- package/src/core/proof/proof-redaction.test-helper.mjs +9 -0
- package/src/core/proof/route-adapter.mjs +74 -0
- package/src/core/proof/route-proof-gate.mjs +33 -0
- package/src/core/proof/route-proof-policy.mjs +96 -0
- package/src/core/proof/selftest-proof-fixtures.mjs +54 -0
- package/src/core/proof/validation.mjs +1 -0
- package/src/core/rust-accelerator.mjs +29 -7
- package/src/core/version.mjs +1 -1
- package/src/core/wiki-image/callout-parser.mjs +16 -0
- package/src/core/wiki-image/computer-use-ledger.mjs +38 -0
- package/src/core/wiki-image/image-relation.mjs +2 -0
- package/src/core/wiki-image/image-voxel-ledger.mjs +43 -0
- package/src/core/wiki-image/proof-linker.mjs +12 -0
- package/src/core/wiki-image/validation.mjs +16 -5
- package/src/core/wiki-image/visual-anchor.mjs +14 -1
package/README.md
CHANGED
|
@@ -30,7 +30,13 @@ sks root
|
|
|
30
30
|
sks
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
`0.9.
|
|
33
|
+
`0.9.13` connects serious routes to Completion Proof, visual routes to image voxel anchors, hook replay to shared runtime policy, codex-lb launch failures to circuit health, and Rust accelerator commands to JS fallback parity. 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
|
+
|
|
35
|
+
Learn more:
|
|
36
|
+
- Completion Proof: [docs/completion-proof.md](docs/completion-proof.md)
|
|
37
|
+
- Image Voxel TriWiki: [docs/image-voxel-ledger.md](docs/image-voxel-ledger.md)
|
|
38
|
+
- Codex App Hooks/PAT: [docs/hooks-pat.md](docs/hooks-pat.md)
|
|
39
|
+
- codex-lb: [docs/codex-lb.md](docs/codex-lb.md)
|
|
34
40
|
|
|
35
41
|
`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.
|
|
36
42
|
|
|
@@ -207,7 +213,7 @@ sks codex-lb repair
|
|
|
207
213
|
sks
|
|
208
214
|
```
|
|
209
215
|
|
|
210
|
-
Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, writes the upstream codex-lb Codex CLI / IDE Extension provider block into `~/.codex/config.toml` for Codex App routing, loads the provider env key for tmux launches, and syncs the macOS user launch environment so the Codex App can see `CODEX_LB_API_KEY` after restart. If the provider block disappears but the stored env file is still recoverable, bare `sks`, npm postinstall upgrades, `sks doctor --fix`, and `sks codex-lb repair` restore it with `env_key = "CODEX_LB_API_KEY"`, `supports_websockets = true`, and `requires_openai_auth = true` as documented by codex-lb. If an older SKS release left the codex-lb dashboard key only in the shared Codex `auth.json` login cache, SKS migrates that key back into `~/.codex/sks-codex-lb.env` when a codex-lb provider or env base URL is already recoverable. It does not rewrite the shared Codex `auth.json` login cache by default; set `SKS_CODEX_LB_SYNC_CODEX_LOGIN=1` only if you intentionally want the old API-key login-cache behavior. When codex-lb is active, SKS opens a fresh `sks-codex-lb-*` tmux session and sweeps older detached codex-lb sessions for the same repo before launch so stale Responses API chains are not reused. Configured launch paths
|
|
216
|
+
Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, writes the upstream codex-lb Codex CLI / IDE Extension provider block into `~/.codex/config.toml` for Codex App routing, loads the provider env key for tmux launches, and syncs the macOS user launch environment so the Codex App can see `CODEX_LB_API_KEY` after restart. If the provider block disappears but the stored env file is still recoverable, bare `sks`, npm postinstall upgrades, `sks doctor --fix`, and `sks codex-lb repair` restore it with `env_key = "CODEX_LB_API_KEY"`, `supports_websockets = true`, and `requires_openai_auth = true` as documented by codex-lb. If an older SKS release left the codex-lb dashboard key only in the shared Codex `auth.json` login cache, SKS migrates that key back into `~/.codex/sks-codex-lb.env` when a codex-lb provider or env base URL is already recoverable. It does not rewrite the shared Codex `auth.json` login cache by default; set `SKS_CODEX_LB_SYNC_CODEX_LOGIN=1` only if you intentionally want the old API-key login-cache behavior. When codex-lb is active, SKS opens a fresh `sks-codex-lb-*` tmux session and sweeps older detached codex-lb sessions for the same repo before launch so stale Responses API chains are not reused. Configured launch paths run a response-chain health check. `previous_response_not_found` is treated as a stateless-LB warning and keeps codex-lb active. Hard failures are surfaced to the user; SKS only bypasses codex-lb when the user chooses OAuth fallback or `SKS_CODEX_LB_AUTOBYPASS=1` is set.
|
|
211
217
|
|
|
212
218
|
If codex-lb provider auth drifts after launch/reinstall, run `sks doctor --fix` or `sks codex-lb repair`; to replace it, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
|
|
213
219
|
|
|
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
|
|
|
4
4
|
fn main() {
|
|
5
5
|
let mut args = std::env::args().skip(1);
|
|
6
6
|
match args.next().as_deref() {
|
|
7
|
-
Some("--version") => println!("sks-rs 0.9.
|
|
7
|
+
Some("--version") => println!("sks-rs 0.9.13"),
|
|
8
8
|
Some("compact-info") => {
|
|
9
9
|
let mut input = String::new();
|
|
10
10
|
let _ = io::stdin().read_to_string(&mut input);
|
|
@@ -28,6 +28,30 @@ fn main() {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
Some("image-hash") => {
|
|
32
|
+
let path = args.next().unwrap_or_default();
|
|
33
|
+
match image_hash(&path) {
|
|
34
|
+
Ok((sha, bytes)) => println!("{{\"ok\":true,\"engine\":\"rust\",\"path\":\"{}\",\"sha256\":\"{}\",\"bytes\":{}}}", json_escape(&path), sha, bytes),
|
|
35
|
+
Err(err) => {
|
|
36
|
+
println!("{{\"ok\":false,\"engine\":\"rust\",\"path\":\"{}\",\"error\":\"{}\"}}", json_escape(&path), json_escape(&err.to_string()));
|
|
37
|
+
std::process::exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
Some("voxel-validate") => {
|
|
42
|
+
let path = args.next().unwrap_or_default();
|
|
43
|
+
match std::fs::read_to_string(&path) {
|
|
44
|
+
Ok(text) => {
|
|
45
|
+
let report = voxel_validate(&text);
|
|
46
|
+
println!("{}", report);
|
|
47
|
+
if report.contains("\"ok\":false") { std::process::exit(1); }
|
|
48
|
+
}
|
|
49
|
+
Err(err) => {
|
|
50
|
+
println!("{{\"ok\":false,\"engine\":\"rust\",\"issues\":[\"read_error\"],\"error\":\"{}\"}}", json_escape(&err.to_string()));
|
|
51
|
+
std::process::exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
31
55
|
Some("secret-scan") => {
|
|
32
56
|
let path = args.next().unwrap_or_default();
|
|
33
57
|
match std::fs::read_to_string(&path) {
|
|
@@ -45,12 +69,128 @@ fn main() {
|
|
|
45
69
|
}
|
|
46
70
|
}
|
|
47
71
|
_ => {
|
|
48
|
-
eprintln!("sks-rs optional accelerator. Commands: --version, compact-info, jsonl-tail, secret-scan");
|
|
72
|
+
eprintln!("sks-rs optional accelerator. Commands: --version, compact-info, jsonl-tail, secret-scan, image-hash, voxel-validate");
|
|
49
73
|
std::process::exit(2);
|
|
50
74
|
}
|
|
51
75
|
}
|
|
52
76
|
}
|
|
53
77
|
|
|
78
|
+
fn image_hash(path: &str) -> io::Result<(String, u64)> {
|
|
79
|
+
let mut file = File::open(path)?;
|
|
80
|
+
let mut data = Vec::new();
|
|
81
|
+
file.read_to_end(&mut data)?;
|
|
82
|
+
let bytes = data.len() as u64;
|
|
83
|
+
Ok((sha256_hex(&data), bytes))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn sha256_hex(data: &[u8]) -> String {
|
|
87
|
+
const K: [u32; 64] = [
|
|
88
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
89
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
90
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
91
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
92
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
93
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
94
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
95
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
|
96
|
+
];
|
|
97
|
+
let mut h: [u32; 8] = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
|
98
|
+
let bit_len = (data.len() as u64) * 8;
|
|
99
|
+
let mut msg = data.to_vec();
|
|
100
|
+
msg.push(0x80);
|
|
101
|
+
while (msg.len() % 64) != 56 { msg.push(0); }
|
|
102
|
+
msg.extend_from_slice(&bit_len.to_be_bytes());
|
|
103
|
+
for chunk in msg.chunks(64) {
|
|
104
|
+
let mut w = [0u32; 64];
|
|
105
|
+
for i in 0..16 {
|
|
106
|
+
w[i] = u32::from_be_bytes([chunk[i * 4], chunk[i * 4 + 1], chunk[i * 4 + 2], chunk[i * 4 + 3]]);
|
|
107
|
+
}
|
|
108
|
+
for i in 16..64 {
|
|
109
|
+
let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
|
|
110
|
+
let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
|
|
111
|
+
w[i] = w[i - 16].wrapping_add(s0).wrapping_add(w[i - 7]).wrapping_add(s1);
|
|
112
|
+
}
|
|
113
|
+
let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh) = (h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]);
|
|
114
|
+
for i in 0..64 {
|
|
115
|
+
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
|
|
116
|
+
let ch = (e & f) ^ ((!e) & g);
|
|
117
|
+
let temp1 = hh.wrapping_add(s1).wrapping_add(ch).wrapping_add(K[i]).wrapping_add(w[i]);
|
|
118
|
+
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
|
|
119
|
+
let maj = (a & b) ^ (a & c) ^ (b & c);
|
|
120
|
+
let temp2 = s0.wrapping_add(maj);
|
|
121
|
+
hh = g;
|
|
122
|
+
g = f;
|
|
123
|
+
f = e;
|
|
124
|
+
e = d.wrapping_add(temp1);
|
|
125
|
+
d = c;
|
|
126
|
+
c = b;
|
|
127
|
+
b = a;
|
|
128
|
+
a = temp1.wrapping_add(temp2);
|
|
129
|
+
}
|
|
130
|
+
h[0] = h[0].wrapping_add(a);
|
|
131
|
+
h[1] = h[1].wrapping_add(b);
|
|
132
|
+
h[2] = h[2].wrapping_add(c);
|
|
133
|
+
h[3] = h[3].wrapping_add(d);
|
|
134
|
+
h[4] = h[4].wrapping_add(e);
|
|
135
|
+
h[5] = h[5].wrapping_add(f);
|
|
136
|
+
h[6] = h[6].wrapping_add(g);
|
|
137
|
+
h[7] = h[7].wrapping_add(hh);
|
|
138
|
+
}
|
|
139
|
+
h.iter().map(|x| format!("{:08x}", x)).collect::<String>()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn voxel_validate(text: &str) -> String {
|
|
143
|
+
let mut issues: Vec<&str> = Vec::new();
|
|
144
|
+
if !text.contains("\"schema\"") || !text.contains("sks.image-voxel-ledger.v1") { issues.push("schema"); }
|
|
145
|
+
if !text.contains("\"images\"") { issues.push("missing_images"); }
|
|
146
|
+
if !text.contains("\"anchors\"") { issues.push("missing_anchors"); }
|
|
147
|
+
let image_count = count_object_entries(text, "images");
|
|
148
|
+
let anchor_count = count_object_entries(text, "anchors");
|
|
149
|
+
if image_count == 0 { issues.push("missing_images"); }
|
|
150
|
+
if anchor_count == 0 { issues.push("missing_anchors"); }
|
|
151
|
+
issues.sort();
|
|
152
|
+
issues.dedup();
|
|
153
|
+
let ok = issues.is_empty();
|
|
154
|
+
let issue_json = issues.iter().map(|x| format!("\"{}\"", json_escape(x))).collect::<Vec<_>>().join(",");
|
|
155
|
+
format!("{{\"ok\":{},\"engine\":\"rust\",\"schema\":\"sks.image-voxel-ledger.v1\",\"images\":{},\"anchors\":{},\"issues\":[{}]}}", if ok { "true" } else { "false" }, image_count, anchor_count, issue_json)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn count_object_entries(text: &str, key: &str) -> usize {
|
|
159
|
+
let marker = format!("\"{}\"", key);
|
|
160
|
+
let Some(start) = text.find(&marker) else { return 0; };
|
|
161
|
+
let Some(open) = text[start..].find('[').map(|i| start + i) else { return 0; };
|
|
162
|
+
let mut depth = 0i32;
|
|
163
|
+
let mut entries = 0usize;
|
|
164
|
+
let mut in_string = false;
|
|
165
|
+
let mut escape = false;
|
|
166
|
+
for ch in text[open..].chars() {
|
|
167
|
+
if escape {
|
|
168
|
+
escape = false;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if ch == '\\' && in_string {
|
|
172
|
+
escape = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if ch == '"' {
|
|
176
|
+
in_string = !in_string;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if in_string { continue; }
|
|
180
|
+
if ch == '[' { depth += 1; }
|
|
181
|
+
else if ch == ']' {
|
|
182
|
+
depth -= 1;
|
|
183
|
+
if depth == 0 { break; }
|
|
184
|
+
}
|
|
185
|
+
else if ch == '{' && depth == 1 { entries += 1; }
|
|
186
|
+
}
|
|
187
|
+
entries
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fn json_escape(value: &str) -> String {
|
|
191
|
+
value.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n").replace('\r', "\\r")
|
|
192
|
+
}
|
|
193
|
+
|
|
54
194
|
fn tail_file(path: &str, bytes: u64) -> io::Result<String> {
|
|
55
195
|
let mut file = File::open(path)?;
|
|
56
196
|
let len = file.metadata()?.len();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.13",
|
|
5
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",
|
|
@@ -40,17 +40,19 @@
|
|
|
40
40
|
"packcheck": "find bin src scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check",
|
|
41
41
|
"changelog:check": "node ./scripts/changelog-check.mjs",
|
|
42
42
|
"cli-entrypoint:check": "node ./scripts/check-cli-entrypoint.mjs",
|
|
43
|
+
"legacy-budget:check": "node ./scripts/check-legacy-budget.mjs",
|
|
43
44
|
"sizecheck": "node ./scripts/sizecheck.mjs",
|
|
44
45
|
"registry:check": "node ./scripts/release-registry-check.mjs",
|
|
45
46
|
"feature:check": "node ./bin/sks.mjs features check --json",
|
|
46
47
|
"all-features:selftest": "node ./bin/sks.mjs all-features selftest --mock --json",
|
|
48
|
+
"all-features:execute-fixtures": "node ./bin/sks.mjs all-features selftest --mock --execute-fixtures --json",
|
|
47
49
|
"perf:cold-start": "node ./bin/sks.mjs perf cold-start --json",
|
|
48
50
|
"perf:gate": "node ./scripts/perf-gate.mjs",
|
|
49
51
|
"test": "node --test \"test/**/*.test.mjs\"",
|
|
50
52
|
"test:unit": "node --test \"test/unit/**/*.test.mjs\"",
|
|
51
53
|
"test:integration:mock": "node --test \"test/integration/**/*.test.mjs\"",
|
|
52
54
|
"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",
|
|
55
|
+
"release:check": "npm run repo-audit && npm run changelog:check && npm run cli-entrypoint:check && npm run legacy-budget:check && npm run packcheck && npm run feature:check && npm run all-features:selftest && npm run all-features:execute-fixtures && npm run selftest && npm run test:unit && npm run test:integration:mock && npm run perf:gate && npm run sizecheck && npm run registry:check",
|
|
54
56
|
"publish:dry": "npm run release:check && npm --cache /tmp/sks-npm-cache publish --dry-run --registry https://registry.npmjs.org/ --access public",
|
|
55
57
|
"publish:npm": "npm --cache /tmp/sks-npm-cache publish --registry https://registry.npmjs.org/ --access public",
|
|
56
58
|
"prepublishOnly": "npm run release:check && node ./scripts/release-registry-check.mjs --require-unpublished"
|
|
@@ -67,7 +67,7 @@ export const COMMANDS = {
|
|
|
67
67
|
'update-check': { maturity: 'stable', summary: 'Check npm package freshness', lazy: legacy },
|
|
68
68
|
usage: { maturity: 'stable', summary: 'Show focused usage topic', lazy: legacy },
|
|
69
69
|
quickstart: { maturity: 'stable', summary: 'Show quickstart flow', lazy: legacy },
|
|
70
|
-
'codex-app': { maturity: 'beta', summary: 'Check Codex App readiness', lazy:
|
|
70
|
+
'codex-app': { maturity: 'beta', summary: 'Check Codex App readiness', lazy: () => import('../commands/codex-app.mjs') },
|
|
71
71
|
openclaw: { maturity: 'labs', summary: 'Create OpenClaw skill package', lazy: legacy },
|
|
72
72
|
bootstrap: { maturity: 'stable', summary: 'Initialize SKS project files', lazy: legacy },
|
|
73
73
|
deps: { maturity: 'stable', summary: 'Check or install local dependencies', lazy: legacy },
|
|
@@ -87,7 +87,7 @@ export const COMMANDS = {
|
|
|
87
87
|
aliases: { maturity: 'stable', summary: 'Show command aliases', lazy: legacy },
|
|
88
88
|
setup: { maturity: 'stable', summary: 'Initialize SKS state', lazy: legacy },
|
|
89
89
|
'fix-path': { maturity: 'stable', summary: 'Repair hook command paths', lazy: legacy },
|
|
90
|
-
doctor: { maturity: 'stable', summary: 'Check and repair SKS install', lazy:
|
|
90
|
+
doctor: { maturity: 'stable', summary: 'Check and repair SKS install', lazy: () => import('../commands/doctor.mjs') },
|
|
91
91
|
init: { maturity: 'stable', summary: 'Initialize local control surface', lazy: legacy },
|
|
92
92
|
selftest: { maturity: 'stable', summary: 'Run local mock selftest', lazy: legacy },
|
|
93
93
|
goal: { maturity: 'beta', summary: 'Manage Goal bridge workflow', lazy: legacy },
|
|
@@ -102,7 +102,7 @@ export const COMMANDS = {
|
|
|
102
102
|
memory: { maturity: 'labs', summary: 'Run retention checks', lazy: legacy },
|
|
103
103
|
gx: { maturity: 'labs', summary: 'Render/validate GX cartridges', lazy: legacy },
|
|
104
104
|
team: { maturity: 'beta', summary: 'Create and observe Team missions', lazy: legacy },
|
|
105
|
-
db: { maturity: 'beta', summary: 'Inspect DB safety policy', lazy:
|
|
105
|
+
db: { maturity: 'beta', summary: 'Inspect DB safety policy', lazy: () => import('../commands/db.mjs') },
|
|
106
106
|
eval: { maturity: 'labs', summary: 'Run eval reports', lazy: legacy },
|
|
107
107
|
harness: { maturity: 'labs', summary: 'Run harness fixtures', lazy: legacy },
|
|
108
108
|
gc: { maturity: 'labs', summary: 'Compact/prune runtime state', lazy: legacy },
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
-
import
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { exists, projectRoot, readJson, writeJsonAtomic } from '../core/fsx.mjs';
|
|
4
5
|
import { CODEX_ACCESS_TOKENS_DOCS_URL } from '../core/codex-app.mjs';
|
|
5
6
|
import { redactSecrets } from '../core/secret-redaction.mjs';
|
|
7
|
+
import { evaluateHookPayload } from '../core/hooks-runtime.mjs';
|
|
6
8
|
import { buildAllFeaturesSelftest, buildFeatureRegistry, validateFeatureRegistry, writeFeatureInventoryDocs } from '../core/feature-registry.mjs';
|
|
7
9
|
|
|
8
10
|
const flag = (args, name) => args.includes(name);
|
|
@@ -44,13 +46,13 @@ export async function featuresCommand(sub = 'list', args = []) {
|
|
|
44
46
|
export async function allFeaturesCommand(sub = 'selftest', args = []) {
|
|
45
47
|
const action = sub || 'selftest';
|
|
46
48
|
if (action !== 'selftest') {
|
|
47
|
-
console.error('Usage: sks all-features selftest --mock [--json]');
|
|
49
|
+
console.error('Usage: sks all-features selftest --mock [--execute-fixtures] [--json]');
|
|
48
50
|
process.exitCode = 1;
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
const root = await projectRoot();
|
|
52
54
|
const registry = await buildFeatureRegistry({ root });
|
|
53
|
-
const result = buildAllFeaturesSelftest(registry);
|
|
55
|
+
const result = buildAllFeaturesSelftest(registry, { executeFixtures: flag(args, '--execute-fixtures'), root });
|
|
54
56
|
if (flag(args, '--json')) console.log(JSON.stringify(result, null, 2));
|
|
55
57
|
else {
|
|
56
58
|
console.log('SKS all-features selftest');
|
|
@@ -139,22 +141,51 @@ async function hooksTrustReport(root) {
|
|
|
139
141
|
|
|
140
142
|
async function hooksReplayReport(fixturePath) {
|
|
141
143
|
if (!fixturePath) return { schema: 'sks.hooks-replay.v1', ok: false, reason: 'fixture_required' };
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
+
const absolute = path.resolve(fixturePath);
|
|
145
|
+
const fixture = await readJson(absolute, {});
|
|
146
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'sks-hooks-replay-'));
|
|
147
|
+
const payload = { ...(fixture.payload || fixture), cwd: tempRoot };
|
|
148
|
+
const state = fixture.state || {};
|
|
149
|
+
if (state.mission_id && fixture.proof) {
|
|
150
|
+
await writeJsonAtomic(path.join(tempRoot, '.sneakoscope', 'missions', state.mission_id, 'completion-proof.json'), fixture.proof);
|
|
151
|
+
}
|
|
144
152
|
const event = fixture.event || fixture.hook_event_name || fixture.name || 'unknown';
|
|
145
|
-
const
|
|
146
|
-
const
|
|
153
|
+
const hookName = normalizeReplayHookName(event);
|
|
154
|
+
const runtime = await evaluateHookPayload(hookName, payload, { root: tempRoot, state });
|
|
155
|
+
const decision = runtime.decision || runtime.permissionDecision || (runtime.continue === true ? 'continue' : 'continue');
|
|
156
|
+
const expected = await readExpectedReplay(absolute);
|
|
157
|
+
const comparable = { decision, reason: runtime.reason || 'fixture_safe' };
|
|
158
|
+
const matchesExpected = expected ? expected.decision === comparable.decision && (!expected.reason || expected.reason === comparable.reason) : null;
|
|
159
|
+
const ok = expected ? matchesExpected : decision !== 'block' && decision !== 'deny';
|
|
147
160
|
return redactSecrets({
|
|
148
161
|
schema: 'sks.hooks-replay.v1',
|
|
149
|
-
ok
|
|
162
|
+
ok,
|
|
150
163
|
event,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
hook: hookName,
|
|
165
|
+
command: payload.command || payload.tool_input?.command || payload.toolInput?.command || payload.input?.command || '',
|
|
166
|
+
decision,
|
|
167
|
+
reason: runtime.reason || 'fixture_safe',
|
|
168
|
+
matches_expected: matchesExpected,
|
|
154
169
|
secret_policy: 'redacted'
|
|
155
170
|
});
|
|
156
171
|
}
|
|
157
172
|
|
|
173
|
+
function normalizeReplayHookName(event = '') {
|
|
174
|
+
const normalized = String(event || '').replace(/[_\s]+/g, '-').toLowerCase();
|
|
175
|
+
if (normalized.includes('pretool') || normalized.includes('pre-tool')) return 'pre-tool';
|
|
176
|
+
if (normalized.includes('permission')) return 'permission-request';
|
|
177
|
+
if (normalized.includes('userprompt') || normalized.includes('user-prompt')) return 'user-prompt-submit';
|
|
178
|
+
if (normalized.includes('posttool') || normalized.includes('post-tool')) return 'post-tool';
|
|
179
|
+
if (normalized.includes('stop')) return 'stop';
|
|
180
|
+
return normalized || 'pre-tool';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function readExpectedReplay(fixturePath) {
|
|
184
|
+
const expectedPath = path.join(path.dirname(fixturePath), 'expected', `${path.basename(fixturePath, '.json')}.expected.json`);
|
|
185
|
+
if (!await exists(expectedPath)) return null;
|
|
186
|
+
return readJson(expectedPath, null);
|
|
187
|
+
}
|
|
188
|
+
|
|
158
189
|
export function hooksExplainReport() {
|
|
159
190
|
return {
|
|
160
191
|
schema: 'sks.hooks-explain.v1',
|
|
@@ -10,6 +10,7 @@ import { initProject, installSkills } from '../core/init.mjs';
|
|
|
10
10
|
import { context7ConfigToml, DOLLAR_SKILL_NAMES, GETDESIGN_REFERENCE, hasContext7ConfigText, RECOMMENDED_SKILLS } from '../core/routes.mjs';
|
|
11
11
|
import { codexLaunchCommand, platformTmuxInstallHint, tmuxReadiness } from '../core/tmux-ui.mjs';
|
|
12
12
|
import { reconcileCodexAppUpgradeProcesses } from '../core/codex-app.mjs';
|
|
13
|
+
import { recordCodexLbHealthEvent } from '../core/codex-lb-circuit.mjs';
|
|
13
14
|
|
|
14
15
|
const DEFAULT_CODEX_APP_PLUGINS = [
|
|
15
16
|
['browser', 'openai-bundled'],
|
|
@@ -513,10 +514,10 @@ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
|
|
|
513
514
|
const env = opts.env || process.env;
|
|
514
515
|
if (!codexLbChainCheckEnabled(env) && !opts.force) return { ok: true, status: 'skipped', skipped: true, reason: 'SKS_CODEX_LB_CHAIN_CHECK=0' };
|
|
515
516
|
const endpoint = codexLbResponsesEndpoint(opts.baseUrl || status.base_url);
|
|
516
|
-
if (!endpoint) return { ok: false, status: 'missing_base_url', chain_unhealthy: true };
|
|
517
|
+
if (!endpoint) return recordCodexLbChainHealth({ ok: false, status: 'missing_base_url', chain_unhealthy: true }, opts);
|
|
517
518
|
const home = opts.home || env.HOME || os.homedir();
|
|
518
519
|
const apiKey = opts.apiKey || parseCodexLbEnvKey(await readText(opts.envPath || status.env_path || codexLbEnvPath(home), ''));
|
|
519
|
-
if (!apiKey) return { ok: false, status: 'missing_env_key', chain_unhealthy: true };
|
|
520
|
+
if (!apiKey) return recordCodexLbChainHealth({ ok: false, status: 'missing_env_key', chain_unhealthy: true }, opts);
|
|
520
521
|
const cached = await readCodexLbChainCache({ endpoint, home, opts, env });
|
|
521
522
|
if (cached) return cached;
|
|
522
523
|
const fetchImpl = opts.fetch || globalThis.fetch;
|
|
@@ -535,19 +536,19 @@ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
|
|
|
535
536
|
};
|
|
536
537
|
const first = await fetchCodexLbResponse(fetchImpl, endpoint, apiKey, baseBody, timeoutMs);
|
|
537
538
|
if (!first.ok || !first.response_id) {
|
|
538
|
-
return writeCodexLbChainCache({
|
|
539
|
+
return recordCodexLbChainHealth(await writeCodexLbChainCache({
|
|
539
540
|
ok: false,
|
|
540
541
|
status: first.ok ? 'missing_response_id' : 'first_request_failed',
|
|
541
542
|
chain_unhealthy: true,
|
|
542
543
|
endpoint,
|
|
543
544
|
http_status: first.status,
|
|
544
545
|
error: redactSecretText(first.error_payload?.error?.message || first.error_payload?.response?.error?.message || first.text || 'codex-lb first Responses request failed', [apiKey])
|
|
545
|
-
}, { endpoint, home, opts, env });
|
|
546
|
+
}, { endpoint, home, opts, env }), opts);
|
|
546
547
|
}
|
|
547
548
|
const second = await fetchCodexLbResponse(fetchImpl, endpoint, apiKey, { ...baseBody, previous_response_id: first.response_id }, timeoutMs);
|
|
548
|
-
if (second.ok) return writeCodexLbChainCache({ ok: true, status: 'chain_ok', endpoint, response_id: first.response_id, chained_response_id: second.response_id || null, http_status: second.status }, { endpoint, home, opts, env });
|
|
549
|
+
if (second.ok) return recordCodexLbChainHealth(await writeCodexLbChainCache({ ok: true, status: 'chain_ok', endpoint, response_id: first.response_id, chained_response_id: second.response_id || null, http_status: second.status }, { endpoint, home, opts, env }), opts);
|
|
549
550
|
const previousMissing = isPreviousResponseNotFound(second.error_payload || second.json || second.text);
|
|
550
|
-
return writeCodexLbChainCache({
|
|
551
|
+
return recordCodexLbChainHealth(await writeCodexLbChainCache({
|
|
551
552
|
ok: false,
|
|
552
553
|
status: previousMissing ? 'previous_response_not_found' : 'second_request_failed',
|
|
553
554
|
chain_unhealthy: true,
|
|
@@ -555,7 +556,13 @@ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
|
|
|
555
556
|
response_id: first.response_id,
|
|
556
557
|
http_status: second.status,
|
|
557
558
|
error: redactSecretText(second.error_payload?.error?.message || second.error_payload?.response?.error?.message || second.text || 'codex-lb chained Responses request failed', [apiKey])
|
|
558
|
-
}, { endpoint, home, opts, env });
|
|
559
|
+
}, { endpoint, home, opts, env }), opts);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function recordCodexLbChainHealth(result, opts = {}) {
|
|
563
|
+
if (!result || result.skipped || opts.recordCircuit === false) return result;
|
|
564
|
+
await recordCodexLbHealthEvent(packageRoot(), result).catch(() => null);
|
|
565
|
+
return result;
|
|
559
566
|
}
|
|
560
567
|
|
|
561
568
|
function hasTopLevelCodexLbSelected(text = '') {
|
package/src/cli/legacy-main.mjs
CHANGED
|
@@ -77,6 +77,7 @@ import { renderTeamDashboardState, writeTeamDashboardState } from '../core/team-
|
|
|
77
77
|
import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
|
|
78
78
|
import { CODEX_APP_DOCS_URL, codexAccessTokenStatus, codexAppIntegrationStatus, findCodexAppUpgradeRepairTargets, formatCodexAppStatus, parseProcessRows } from '../core/codex-app.mjs';
|
|
79
79
|
import { buildAllFeaturesSelftest, buildFeatureRegistry, validateFeatureRegistry } from '../core/feature-registry.mjs';
|
|
80
|
+
import { writeSelftestRouteProof } from '../core/proof/selftest-proof-fixtures.mjs';
|
|
80
81
|
import { codexAppRemoteControlCommand } from './codex-app-command.mjs';
|
|
81
82
|
import { allFeaturesCommand, featuresCommand, hooksCommand, hooksExplainReport } from './feature-commands.mjs';
|
|
82
83
|
import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs';
|
|
@@ -2088,10 +2089,7 @@ function readFlagValue(args, name, fallback) {
|
|
|
2088
2089
|
}
|
|
2089
2090
|
|
|
2090
2091
|
async function selftest() {
|
|
2091
|
-
//
|
|
2092
|
-
// canAskYesNo() (codex-lb provider-restore prompt, chain-failure prompt, etc.) takes the
|
|
2093
|
-
// non-interactive fallback path instead of bubbling a live readline prompt up to the
|
|
2094
|
-
// user's terminal (e.g. during `npm publish` -> prepublishOnly -> release:check -> selftest).
|
|
2092
|
+
// Keep selftest non-interactive even when install helpers would normally prompt.
|
|
2095
2093
|
process.env.CI = 'true';
|
|
2096
2094
|
const tmp = tmpdir();
|
|
2097
2095
|
process.chdir(tmp);
|
|
@@ -2115,6 +2113,7 @@ async function selftest() {
|
|
|
2115
2113
|
if (trippedStop) throw new Error('selftest: compliance loop guard did not terminally trip');
|
|
2116
2114
|
const loopBlocker = await readJson(path.join(loopMission.dir, 'hard-blocker.json'), null);
|
|
2117
2115
|
if (loopBlocker?.reason !== 'compliance_loop_guard_tripped') throw new Error('selftest: compliance loop guard did not write hard blocker');
|
|
2116
|
+
await writeSelftestRouteProof(tmp, { missionId: loopMission.id, kind: 'hard_blocker' });
|
|
2118
2117
|
const hardBlockerUnblocked = await evaluateStop(tmp, loopState, { last_assistant_message: 'done' });
|
|
2119
2118
|
if (hardBlockerUnblocked?.decision === 'block' && !String(hardBlockerUnblocked.reason || '').includes('reflection')) throw new Error('selftest: hard blocker did not unblock incomplete active gate');
|
|
2120
2119
|
const clarificationMission = await createMission(tmp, { mode: 'team', prompt: 'visible question gate selftest' });
|
|
@@ -3406,6 +3405,7 @@ async function selftest() {
|
|
|
3406
3405
|
const missingCleanupStop = await evaluateStop(routeGateTmp, gateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3407
3406
|
if (missingCleanupStop?.decision !== 'block' || !String(missingCleanupStop.reason || '').includes(TEAM_SESSION_CLEANUP_ARTIFACT)) throw new Error('selftest: Team route did not block missing session cleanup gate');
|
|
3408
3407
|
await writeJsonAtomic(path.join(gateDir, TEAM_SESSION_CLEANUP_ARTIFACT), passedTeamSessionCleanup);
|
|
3408
|
+
await writeSelftestRouteProof(routeGateTmp, { missionId: gateId, kind: 'team_gate' });
|
|
3409
3409
|
const missingReflectionStop = await evaluateStop(routeGateTmp, gateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3410
3410
|
if (missingReflectionStop?.decision !== 'block' || !String(missingReflectionStop.reason || '').includes('reflection')) throw new Error('selftest: full route did not block missing reflection gate');
|
|
3411
3411
|
const missingReflectionNoQuestionStop = await evaluateStop(routeGateTmp, gateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: true });
|
|
@@ -3430,6 +3430,7 @@ async function selftest() {
|
|
|
3430
3430
|
await writeJsonAtomic(path.join(subagentGateDir, TEAM_SESSION_CLEANUP_ARTIFACT), passedTeamSessionCleanup);
|
|
3431
3431
|
await writeTextAtomic(path.join(subagentGateDir, REFLECTION_ARTIFACT), '# Post-Route Reflection\n\nNo issue selftest.\n');
|
|
3432
3432
|
await writeJsonAtomic(path.join(subagentGateDir, REFLECTION_GATE), { schema_version: 1, passed: true, mission_id: subagentGateId, route: '$Team', reflection_artifact: true, lessons_recorded: false, no_issue_acknowledged: true, triwiki_recorded: false, wiki_refreshed_or_packed: true, wiki_validated: true, created_at: nowIso() });
|
|
3433
|
+
await writeSelftestRouteProof(subagentGateTmp, { missionId: subagentGateId, kind: 'subagent_gate' });
|
|
3433
3434
|
const subagentUnblocked = await evaluateStop(subagentGateTmp, subagentGateState, { last_assistant_message: 'SKS Honest Mode verification evidence gap' }, { noQuestion: false });
|
|
3434
3435
|
if (subagentUnblocked?.decision === 'block') throw new Error('selftest: subagent evidence did not unblock route gate');
|
|
3435
3436
|
const { id: teamId, dir: teamDir } = await createMission(tmp, { mode: 'team', prompt: '병렬 구현 팀 테스트' });
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { flag } from '../cli/args.mjs';
|
|
2
|
+
import { printJson } from '../cli/output.mjs';
|
|
3
|
+
import { codexAccessTokenStatus, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
|
|
4
|
+
import { codexAppRemoteControlCommand } from '../cli/codex-app-command.mjs';
|
|
5
|
+
|
|
6
|
+
export async function run(_command, args = []) {
|
|
7
|
+
const action = args[0] || 'check';
|
|
8
|
+
if (action === 'remote-control' || action === 'remote') return codexAppRemoteControlCommand(args.slice(1));
|
|
9
|
+
if (action === 'pat') {
|
|
10
|
+
const status = codexAccessTokenStatus();
|
|
11
|
+
if (flag(args, '--json')) return printJson(status);
|
|
12
|
+
console.log('Codex App PAT status');
|
|
13
|
+
console.log(`Status: ${status.status}`);
|
|
14
|
+
for (const entry of status.access_token_env_vars) console.log(`${entry.name}: ${entry.present ? entry.value : 'missing'}`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (action === 'check' || action === 'status') {
|
|
18
|
+
const status = await codexAppIntegrationStatus();
|
|
19
|
+
if (flag(args, '--json')) {
|
|
20
|
+
printJson(status);
|
|
21
|
+
if (!status.ok) process.exitCode = 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
console.log(formatCodexAppStatus(status, { includeRaw: flag(args, '--verbose') }));
|
|
25
|
+
if (!status.ok) process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.error('Usage: sks codex-app check|status|pat status|remote-control [--json]');
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { projectRoot } from '../core/fsx.mjs';
|
|
2
|
-
import { flag } from '../cli/args.mjs';
|
|
3
|
+
import { flag, readOption } from '../cli/args.mjs';
|
|
3
4
|
import { printJson } from '../cli/output.mjs';
|
|
4
|
-
import { codexLbMetrics, readCodexLbCircuit, resetCodexLbCircuit } from '../core/codex-lb-circuit.mjs';
|
|
5
|
+
import { codexLbMetrics, readCodexLbCircuit, recordCodexLbHealthEvent, resetCodexLbCircuit } from '../core/codex-lb-circuit.mjs';
|
|
5
6
|
|
|
6
7
|
export async function run(command, args = []) {
|
|
7
8
|
const root = await projectRoot();
|
|
@@ -26,6 +27,21 @@ export async function run(command, args = []) {
|
|
|
26
27
|
console.log('codex-lb circuit reset');
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
30
|
+
if (action === 'circuit' && args[1] === 'record-fixture') {
|
|
31
|
+
const fixturePath = args[2] || readOption(args, '--fixture', null);
|
|
32
|
+
if (!fixturePath) {
|
|
33
|
+
console.error('Usage: sks codex-lb circuit record-fixture <fixture.json> [--json]');
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const { readJson } = await import('../core/fsx.mjs');
|
|
38
|
+
const event = await readJson(path.isAbsolute(fixturePath) ? fixturePath : path.resolve(root, fixturePath), {});
|
|
39
|
+
const circuit = await recordCodexLbHealthEvent(root, event);
|
|
40
|
+
const result = { schema: 'sks.codex-lb-circuit-record-fixture.v1', ok: true, fixture: fixturePath, circuit };
|
|
41
|
+
if (flag(args, '--json')) return printJson(result);
|
|
42
|
+
console.log(`codex-lb circuit: ${circuit.state}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
29
45
|
const legacy = await import('../cli/legacy-main.mjs');
|
|
30
46
|
return legacy.main([command, ...args]);
|
|
31
47
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { projectRoot, dirSize, exists, formatBytes } from '../core/fsx.mjs';
|
|
2
|
+
import { flag } from '../cli/args.mjs';
|
|
3
|
+
import { printJson } from '../cli/output.mjs';
|
|
4
|
+
import { getCodexInfo } from '../core/codex-adapter.mjs';
|
|
5
|
+
import { rustInfo } from '../core/rust-accelerator.mjs';
|
|
6
|
+
import { codexAppIntegrationStatus } from '../core/codex-app.mjs';
|
|
7
|
+
import { codexLbMetrics, readCodexLbCircuit } from '../core/codex-lb-circuit.mjs';
|
|
8
|
+
|
|
9
|
+
export async function run(_command, args = []) {
|
|
10
|
+
if (flag(args, '--fix')) {
|
|
11
|
+
const legacy = await import('../cli/legacy-main.mjs');
|
|
12
|
+
return legacy.main(['doctor', ...args]);
|
|
13
|
+
}
|
|
14
|
+
const root = await projectRoot();
|
|
15
|
+
const codex = await getCodexInfo().catch((err) => ({ available: false, error: err.message }));
|
|
16
|
+
const rust = await rustInfo().catch((err) => ({ available: false, error: err.message }));
|
|
17
|
+
const codexApp = await codexAppIntegrationStatus({ codex }).catch((err) => ({ ok: false, error: err.message }));
|
|
18
|
+
const codexLb = codexLbMetrics(await readCodexLbCircuit(root).catch(() => ({})));
|
|
19
|
+
const pkgBytes = await dirSize(root).catch(() => 0);
|
|
20
|
+
const result = {
|
|
21
|
+
schema: 'sks.doctor-status.v1',
|
|
22
|
+
ok: Boolean(codex.bin) && codexApp.ok && codexLb.ok,
|
|
23
|
+
root,
|
|
24
|
+
node: { ok: Number(process.versions.node.split('.')[0]) >= 20, version: process.version },
|
|
25
|
+
codex,
|
|
26
|
+
rust,
|
|
27
|
+
codex_app: codexApp,
|
|
28
|
+
codex_lb: codexLb,
|
|
29
|
+
sneakoscope: { ok: await exists(`${root}/.sneakoscope`) },
|
|
30
|
+
package: { bytes: pkgBytes, human: formatBytes(pkgBytes) }
|
|
31
|
+
};
|
|
32
|
+
if (flag(args, '--json')) {
|
|
33
|
+
printJson(result);
|
|
34
|
+
if (!result.ok) process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log('SKS Doctor');
|
|
38
|
+
console.log(`Root: ${root}`);
|
|
39
|
+
console.log(`Node: ${result.node.ok ? 'ok' : 'fail'} ${result.node.version}`);
|
|
40
|
+
console.log(`Codex: ${codex.bin ? 'ok' : 'missing'} ${codex.version || ''}`);
|
|
41
|
+
console.log(`Rust acc.: ${rust.available ? rust.version : 'optional-missing'}`);
|
|
42
|
+
console.log(`Codex App: ${codexApp.ok ? 'ok' : 'needs setup'}`);
|
|
43
|
+
console.log(`codex-lb: ${codexLb.ok ? 'ok' : `blocked ${codexLb.circuit?.state || 'unknown'}`}`);
|
|
44
|
+
console.log(`Ready: ${result.ok ? 'yes' : 'no'}`);
|
|
45
|
+
if (!result.ok) process.exitCode = 1;
|
|
46
|
+
}
|