sneakoscope 9.2.6 → 10.0.0
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 +2 -2
- package/config/skills-hash-ledger.v1.json +4 -0
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/bin/fast-inline.js +57 -8
- package/dist/bin/sks-dispatch.js +1 -1
- package/dist/commands/doctor.js +6 -1
- package/dist/config/skills-manifest.json +9 -5
- package/dist/core/codex-hooks/codex-hook-managed-install.js +4 -2
- package/dist/core/codex-lb/desktop-bridge/server.js +1 -1
- package/dist/core/codex-lb/desktop-bridge/state.js +3 -1
- package/dist/core/codex-lb/desktop-bridge/upstream-evidence.js +42 -0
- package/dist/core/daemon/sksd-hook-daemon-entrypoint.js +1 -1
- package/dist/core/daemon/sksd-hook-daemon.js +14 -2
- package/dist/core/doctor/desktop-bridge-catalog-repair.js +27 -45
- package/dist/core/doctor/doctor-readiness-matrix.js +23 -4
- package/dist/core/hooks-runtime/hook-context.js +5 -2
- package/dist/core/hooks-runtime.js +22 -16
- package/dist/core/init/skills.js +4 -4
- package/dist/core/init.js +22 -8
- package/dist/core/verification-profile.js +99 -0
- package/dist/core/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, mult
|
|
|
22
22
|
Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
|
|
23
23
|
<!-- END SKS SEARCH VISIBILITY MARKETING -->
|
|
24
24
|
|
|
25
|
-
This README documents package **SKS
|
|
25
|
+
This README documents package **SKS 10.0.0** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
|
|
26
26
|
|
|
27
27
|
Use the official latest stable SKS and Codex CLI releases. The Codex compatibility SSOT is always the **current latest stable** host; capability probes measure what that host can actually do. Product docs do not crown a fixed `0.x.y` string as SSOT (release pins and schema directories are measured artifacts for the current package, not a permanent product version claim). Menu Bar / Center induce updates to the latest stable build. Run `sks update-check` for what is installed and read the capability report for what is supported. Install SSOT is npm `sneakoscope@latest`; PATH `sks` and Menu Bar stamped generation must match that version or gates fail. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it (Codex official multi-agent wrap-only; SKS does not reimplement a parallel runtime). Local code search is mode-separated (`sks search files|text|structure|symbol|context`); `context` is answered by the compiled TriWiki Context Graph (`context-graph.json` is exhaustive authority; `context-pack.json` and managed `AGENTS.md` are bounded projections) — see [docs/architecture/context-graph.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/architecture/context-graph.md) and [docs/PRODUCT-CONTRACT.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/PRODUCT-CONTRACT.md). See [CHANGELOG.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/CHANGELOG.md).
|
|
28
28
|
|
|
@@ -305,7 +305,7 @@ Adapter rules that keep updates safe:
|
|
|
305
305
|
| --- | --- | --- |
|
|
306
306
|
| Did tests/typecheck fail? | Another model may say so. | Machine check output is tagged `evidence: machine`. |
|
|
307
307
|
| Are findings ranked? | Usually one blended opinion. | Machine evidence sorts before LLM findings. |
|
|
308
|
-
| Can work stop? | The model decides. | Stop gates, Completion Proof, and Honest Mode
|
|
308
|
+
| Can work stop? | The model decides. | Safety gates decide (`essential`, default: a finished turn finishes; `strict` adds Stop gates, Completion Proof, and Honest Mode). |
|
|
309
309
|
| Can I inspect agent-thread progress? | Usually no runtime UI. | Use the official Codex subagent/thread surfaces and their proof artifacts. |
|
|
310
310
|
|
|
311
311
|
## Demo
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
{
|
|
19
19
|
"canonical_name": "sks-answer",
|
|
20
20
|
"trusted_sha256": [
|
|
21
|
+
"0e461537093215960734aaeafdccb125d2c3ca139dab5db8548d5c0bdd14d652",
|
|
21
22
|
"3056ac543fa074868c7023097fdcb042d7175ab557d8e18cb7d92ce872785452",
|
|
22
23
|
"bc8e2fc157b6c9819d32bb0a41bdf7f599a07693df18b0d80aa0ba103594f6eb"
|
|
23
24
|
]
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
{
|
|
66
67
|
"canonical_name": "sks-computer-use-fast",
|
|
67
68
|
"trusted_sha256": [
|
|
69
|
+
"1d191ed0f9490fc320dd17b871ca556fa5835bfb372c38b62f3985e3369f685a",
|
|
68
70
|
"57e4c1d04ae785b0a53711e53210f2ddabd6131041b97edb4b6d99f18b24155e",
|
|
69
71
|
"b270d82e4057934d7f92ed4241d4ae86e22d0a9a08bceba1aa179d7535bda042",
|
|
70
72
|
"b8736b2621398dfc85f7a2ff35d7a2fc728db0dfc0e964de69aebff93b5ed897"
|
|
@@ -207,6 +209,7 @@
|
|
|
207
209
|
{
|
|
208
210
|
"canonical_name": "sks-honest-mode",
|
|
209
211
|
"trusted_sha256": [
|
|
212
|
+
"4a0722737a837b0de895b7c950da216a8641922e4da53d6b9c1681b957340e82",
|
|
210
213
|
"d1216afadecd4d9c85a2fb54bd31ee77ba45f8071f43bb0e21305c5fcd870095",
|
|
211
214
|
"4543d942cac510b1a23e2e842f944d033087bbc761f6abc4a27d0a015bf22235"
|
|
212
215
|
]
|
|
@@ -283,6 +286,7 @@
|
|
|
283
286
|
{
|
|
284
287
|
"canonical_name": "sks-pipeline-runner",
|
|
285
288
|
"trusted_sha256": [
|
|
289
|
+
"66e105773b8e042eec5d5679b97b7cb305cad8d45671034907fbfa728fc4d68a",
|
|
286
290
|
"020ba4da2f96bcda23cee4596b1e790816780178f1917dee748ff4b289cba0e5",
|
|
287
291
|
"ffcbe0ea4b585e3d2886f1d9fa7a3b88706aaf42182fb177b2371b76beb0ddcd"
|
|
288
292
|
]
|
package/dist/bin/fast-inline.js
CHANGED
|
@@ -11,9 +11,63 @@ function rootJsonFastInline(fs, cwd = process.cwd()) {
|
|
|
11
11
|
using_global_root: !project
|
|
12
12
|
})}\n`);
|
|
13
13
|
}
|
|
14
|
+
async function fastDesktopBridgeUpstreamEvidence(home) {
|
|
15
|
+
const notChecked = (reason) => ({
|
|
16
|
+
section: { schema: 'sks.desktop-bridge-fast-status.v1', status: 'not_checked', reason, secret_stores_read: false },
|
|
17
|
+
warnings: [], nextActions: [],
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
const getBuiltinModule = process.getBuiltinModule;
|
|
21
|
+
const fs = typeof getBuiltinModule === 'function' ? getBuiltinModule('node:fs') : await import('node:fs');
|
|
22
|
+
const runtime = joinPath(joinPath(home, '.codex'), 'sks');
|
|
23
|
+
const statePath = joinPath(runtime, 'desktop-bridge-state.json');
|
|
24
|
+
if (!fs.existsSync(statePath))
|
|
25
|
+
return notChecked('bridge_state_missing');
|
|
26
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
27
|
+
const startedAt = typeof state.started_at === 'string' ? state.started_at : null;
|
|
28
|
+
const logPath = joinPath(joinPath(runtime, 'logs'), 'desktop-bridge.out.log');
|
|
29
|
+
const { BRIDGE_LOG_TAIL_BYTES, UNREACHABLE_UPSTREAM_RECOVERY_ACTION, detectUnreachableUpstreamEvidence } = await import('../core/codex-lb/desktop-bridge/upstream-evidence.js');
|
|
30
|
+
let tail = '';
|
|
31
|
+
if (fs.existsSync(logPath)) {
|
|
32
|
+
const handle = fs.openSync(logPath, 'r');
|
|
33
|
+
try {
|
|
34
|
+
const size = fs.fstatSync(handle).size;
|
|
35
|
+
const start = Math.max(0, size - BRIDGE_LOG_TAIL_BYTES);
|
|
36
|
+
const buffer = Buffer.alloc(size - start);
|
|
37
|
+
fs.readSync(handle, buffer, 0, buffer.length, start);
|
|
38
|
+
tail = buffer.toString('utf8');
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
fs.closeSync(handle);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const code = detectUnreachableUpstreamEvidence(tail, startedAt, Date.now());
|
|
45
|
+
const blockers = code ? [`desktop_bridge_upstream_unreachable:${code}`] : [];
|
|
46
|
+
return {
|
|
47
|
+
section: {
|
|
48
|
+
schema: 'sks.desktop-bridge-fast-status.v1',
|
|
49
|
+
status: code ? 'upstream_unreachable_evidence' : 'log_evidence_clear',
|
|
50
|
+
reason: 'fast_readonly_json_log_evidence',
|
|
51
|
+
secret_stores_read: false,
|
|
52
|
+
serving_pid: typeof state.pid === 'number' ? state.pid : null,
|
|
53
|
+
started_at: startedAt,
|
|
54
|
+
blockers,
|
|
55
|
+
recovery_actions: code ? [UNREACHABLE_UPSTREAM_RECOVERY_ACTION] : [],
|
|
56
|
+
},
|
|
57
|
+
warnings: blockers,
|
|
58
|
+
nextActions: code ? [UNREACHABLE_UPSTREAM_RECOVERY_ACTION] : [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return notChecked('evidence_read_failed');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
14
65
|
async function doctorJsonFastInline(input = {}) {
|
|
15
66
|
const startedAt = Date.now();
|
|
16
67
|
const write = input.write || ((text) => process.stdout.write(text));
|
|
68
|
+
const env = input.processEnv || process.env;
|
|
69
|
+
const home = input.home || env.HOME || env.USERPROFILE || process.cwd();
|
|
70
|
+
const bridge = await fastDesktopBridgeUpstreamEvidence(home);
|
|
17
71
|
write(`${JSON.stringify({
|
|
18
72
|
schema: 'sks.doctor-status.v3',
|
|
19
73
|
elapsed_ms: Math.max(0, Date.now() - startedAt),
|
|
@@ -23,7 +77,7 @@ async function doctorJsonFastInline(input = {}) {
|
|
|
23
77
|
deep_diagnostics_skipped: true,
|
|
24
78
|
deep_ok: null,
|
|
25
79
|
not_counted_as_full_doctor: true,
|
|
26
|
-
next_actions: ['Run sks doctor --full --json for deep diagnostics.'],
|
|
80
|
+
next_actions: ['Run sks doctor --full --json for deep diagnostics.', ...bridge.nextActions],
|
|
27
81
|
fast_path: true,
|
|
28
82
|
profile: 'fast-readonly',
|
|
29
83
|
root: process.cwd(),
|
|
@@ -40,13 +94,8 @@ async function doctorJsonFastInline(input = {}) {
|
|
|
40
94
|
},
|
|
41
95
|
doctor_fix_transaction: null,
|
|
42
96
|
blockers: [],
|
|
43
|
-
warnings: ['fast_readonly_doctor_skipped_optional_deep_diagnostics'],
|
|
44
|
-
desktop_bridge:
|
|
45
|
-
schema: 'sks.desktop-bridge-fast-status.v1',
|
|
46
|
-
status: 'not_checked',
|
|
47
|
-
reason: 'fast_readonly_json',
|
|
48
|
-
secret_stores_read: false
|
|
49
|
-
}
|
|
97
|
+
warnings: ['fast_readonly_doctor_skipped_optional_deep_diagnostics', ...bridge.warnings],
|
|
98
|
+
desktop_bridge: bridge.section
|
|
50
99
|
}, null, 2)}\n`);
|
|
51
100
|
}
|
|
52
101
|
async function narutoHelpJsonFastInline() {
|
package/dist/bin/sks-dispatch.js
CHANGED
|
@@ -27,7 +27,7 @@ async function runSks(args) {
|
|
|
27
27
|
const { hookUserPromptSubmitPerfInline } = await import('./fast-inline.js');
|
|
28
28
|
await hookUserPromptSubmitPerfInline();
|
|
29
29
|
}
|
|
30
|
-
else if (args[0] === 'hook' && args[1] &&
|
|
30
|
+
else if (args[0] === 'hook' && args[1] && (await import('../core/verification-profile.js')).hookDaemonEnabled()) {
|
|
31
31
|
const { hookDaemonInline } = await import('../core/daemon/sksd-hook-dispatch.js');
|
|
32
32
|
await hookDaemonInline(args[1]);
|
|
33
33
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -73,6 +73,11 @@ export async function inspectDoctorDesktopBridgeStatus(input = {}, deps = {}) {
|
|
|
73
73
|
? (status.readiness.blockers || []).map(String).filter(Boolean)
|
|
74
74
|
: [];
|
|
75
75
|
const recoveryActions = (status.recovery_actions || []).map(String).filter(Boolean);
|
|
76
|
+
const warnings = (status.readiness.warnings || []).map(String).filter(Boolean);
|
|
77
|
+
if (expected && status.readiness.ready !== true && status.readiness.state === 'degraded' && blockers.length === 0) {
|
|
78
|
+
warnings.push('desktop_bridge_readiness_degraded:transport_unverified_for_current_process');
|
|
79
|
+
recoveryActions.push('Run `sks bridge verify --level transport` (or `sks doctor --fix`, which re-verifies after its repair) to bind a transport diagnostic to the serving bridge.');
|
|
80
|
+
}
|
|
76
81
|
if (expected && status.management?.managed === true) {
|
|
77
82
|
const evidence = await (deps.desktopBridgeUpstreamEvidenceImpl || readDesktopBridgeUnreachableUpstreamEvidence)(home).catch(() => null);
|
|
78
83
|
if (evidence) {
|
|
@@ -89,7 +94,7 @@ export async function inspectDoctorDesktopBridgeStatus(input = {}, deps = {}) {
|
|
|
89
94
|
status,
|
|
90
95
|
providers: status.providers,
|
|
91
96
|
blockers,
|
|
92
|
-
warnings
|
|
97
|
+
warnings,
|
|
93
98
|
recovery_actions: recoveryActions
|
|
94
99
|
};
|
|
95
100
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "sks.skills-manifest.v1",
|
|
3
|
-
"package_version": "
|
|
3
|
+
"package_version": "10.0.0",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
6
6
|
"canonical_name": "sks",
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
{
|
|
24
24
|
"canonical_name": "sks-answer",
|
|
25
25
|
"type": "official",
|
|
26
|
-
"content_sha256": "
|
|
26
|
+
"content_sha256": "0e461537093215960734aaeafdccb125d2c3ca139dab5db8548d5c0bdd14d652",
|
|
27
27
|
"hash_history": [
|
|
28
|
+
"3056ac543fa074868c7023097fdcb042d7175ab557d8e18cb7d92ce872785452",
|
|
28
29
|
"bc8e2fc157b6c9819d32bb0a41bdf7f599a07693df18b0d80aa0ba103594f6eb"
|
|
29
30
|
],
|
|
30
31
|
"deprecated_aliases": []
|
|
@@ -82,8 +83,9 @@
|
|
|
82
83
|
{
|
|
83
84
|
"canonical_name": "sks-computer-use-fast",
|
|
84
85
|
"type": "official",
|
|
85
|
-
"content_sha256": "
|
|
86
|
+
"content_sha256": "1d191ed0f9490fc320dd17b871ca556fa5835bfb372c38b62f3985e3369f685a",
|
|
86
87
|
"hash_history": [
|
|
88
|
+
"57e4c1d04ae785b0a53711e53210f2ddabd6131041b97edb4b6d99f18b24155e",
|
|
87
89
|
"b270d82e4057934d7f92ed4241d4ae86e22d0a9a08bceba1aa179d7535bda042",
|
|
88
90
|
"b8736b2621398dfc85f7a2ff35d7a2fc728db0dfc0e964de69aebff93b5ed897"
|
|
89
91
|
],
|
|
@@ -263,8 +265,9 @@
|
|
|
263
265
|
{
|
|
264
266
|
"canonical_name": "sks-honest-mode",
|
|
265
267
|
"type": "official",
|
|
266
|
-
"content_sha256": "
|
|
268
|
+
"content_sha256": "4a0722737a837b0de895b7c950da216a8641922e4da53d6b9c1681b957340e82",
|
|
267
269
|
"hash_history": [
|
|
270
|
+
"d1216afadecd4d9c85a2fb54bd31ee77ba45f8071f43bb0e21305c5fcd870095",
|
|
268
271
|
"4543d942cac510b1a23e2e842f944d033087bbc761f6abc4a27d0a015bf22235"
|
|
269
272
|
],
|
|
270
273
|
"deprecated_aliases": []
|
|
@@ -351,8 +354,9 @@
|
|
|
351
354
|
{
|
|
352
355
|
"canonical_name": "sks-pipeline-runner",
|
|
353
356
|
"type": "official",
|
|
354
|
-
"content_sha256": "
|
|
357
|
+
"content_sha256": "66e105773b8e042eec5d5679b97b7cb305cad8d45671034907fbfa728fc4d68a",
|
|
355
358
|
"hash_history": [
|
|
359
|
+
"020ba4da2f96bcda23cee4596b1e790816780178f1917dee748ff4b289cba0e5",
|
|
356
360
|
"ffcbe0ea4b585e3d2886f1d9fa7a3b88706aaf42182fb177b2371b76beb0ddcd"
|
|
357
361
|
],
|
|
358
362
|
"deprecated_aliases": []
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { ensureDir, nowIso, readText, writeTextAtomic } from '../fsx.js';
|
|
3
3
|
import { CODEX_HOOK_EVENTS } from '../codex-compat/codex-hook-events.js';
|
|
4
|
+
import { postToolEvidenceEnabled } from '../verification-profile.js';
|
|
4
5
|
import { buildCodexCommandHookToml, matcherApplies } from './codex-hook-config-writer.js';
|
|
5
6
|
import { readCodexHookActualState } from './codex-hook-actual-discovery.js';
|
|
6
7
|
import { writeTrustedHashStateForHooksFile } from './codex-hook-state-writer.js';
|
|
@@ -22,12 +23,13 @@ export async function installManagedCodexHooks(root, opts = {}) {
|
|
|
22
23
|
const scriptPath = path.join(managedDir, 'sks-managed-hook.sh');
|
|
23
24
|
const tomlPath = path.join(managedDir, 'sks-managed-hooks.toml');
|
|
24
25
|
const binCommand = opts.binCommand || 'sks hook';
|
|
25
|
-
const
|
|
26
|
+
const installedEvents = CODEX_HOOK_EVENTS.filter((event) => event !== 'PostToolUse' || postToolEvidenceEnabled(root));
|
|
27
|
+
const hooksToml = installedEvents.map((event) => buildCodexCommandHookToml({
|
|
26
28
|
event,
|
|
27
29
|
matcher: matcherApplies(event) ? '*' : null,
|
|
28
30
|
command: `${scriptPath} ${HOOK_SUBCOMMANDS[event] || event}`,
|
|
29
31
|
timeout: event === 'Stop' ? 60 : 30,
|
|
30
|
-
statusMessage: event === 'Stop' ? 'SKS
|
|
32
|
+
statusMessage: event === 'Stop' ? 'SKS checking done gate' : null
|
|
31
33
|
})).join('\n');
|
|
32
34
|
const requirementsToml = mergeRequirementsToml(await readText(requirementsPath, ''), {
|
|
33
35
|
managedDir,
|
|
@@ -306,7 +306,7 @@ export async function startPreparedDesktopBridge(input, options = {}) {
|
|
|
306
306
|
}
|
|
307
307
|
const freshnessMs = input.stateFreshnessMs ?? 5 * 60_000;
|
|
308
308
|
const heartbeat = statePath ? setInterval(() => {
|
|
309
|
-
void refreshDesktopBridgeState(statePath, state, new Date(), freshnessMs).catch(() => undefined);
|
|
309
|
+
void refreshDesktopBridgeState(statePath, state, new Date(), freshnessMs, { preserveVerifiedProbeIds: true }).catch(() => undefined);
|
|
310
310
|
}, Math.max(1_000, Math.floor(freshnessMs / 3))) : null;
|
|
311
311
|
heartbeat?.unref();
|
|
312
312
|
let skewCandidate = null;
|
|
@@ -69,11 +69,13 @@ export function isDesktopBridgePublicState(value) {
|
|
|
69
69
|
&& Array.isArray(row.last_verified_probe_ids) && row.last_verified_probe_ids.every(isGeneration)
|
|
70
70
|
&& typeof row.config_generation === 'string' && /^[a-f0-9]{64}$/.test(row.config_generation);
|
|
71
71
|
}
|
|
72
|
-
export async function refreshDesktopBridgeState(file, state, now = new Date(), freshnessMs = DEFAULT_FRESHNESS_MS) {
|
|
72
|
+
export async function refreshDesktopBridgeState(file, state, now = new Date(), freshnessMs = DEFAULT_FRESHNESS_MS, options = {}) {
|
|
73
73
|
const current = await readDesktopBridgeState(file).catch(() => null);
|
|
74
74
|
if (!current || current.schema !== DESKTOP_BRIDGE_STATE_SCHEMA || current.pid !== state.pid
|
|
75
75
|
|| current.process_generation !== state.process_generation || current.config_generation !== state.config_generation)
|
|
76
76
|
return false;
|
|
77
|
+
if (options.preserveVerifiedProbeIds)
|
|
78
|
+
state.last_verified_probe_ids = [...current.last_verified_probe_ids];
|
|
77
79
|
state.updated_at = now.toISOString();
|
|
78
80
|
state.stale_after = new Date(now.getTime() + freshnessMs).toISOString();
|
|
79
81
|
await writeDesktopBridgeState(file, state);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const BRIDGE_UNREACHABLE_EVIDENCE_WINDOW_MS = 10 * 60_000;
|
|
2
|
+
export const BRIDGE_LOG_TAIL_BYTES = 256 * 1024;
|
|
3
|
+
const UNREACHABLE_REJECTION_CODE = /^bridge_(?:websocket_)?upstream_unavailable/;
|
|
4
|
+
const REROUTED_REJECTION_CODE = /^bridge_upstream_unreachable_rerouted/;
|
|
5
|
+
export function detectUnreachableUpstreamEvidence(logTail, startedAt, nowMs) {
|
|
6
|
+
const startedMs = startedAt ? Date.parse(startedAt) : Number.NaN;
|
|
7
|
+
const cutoffMs = Math.max(Number.isFinite(startedMs) ? startedMs : 0, nowMs - BRIDGE_UNREACHABLE_EVIDENCE_WINDOW_MS);
|
|
8
|
+
let latest = null;
|
|
9
|
+
let latestReroute = Number.NEGATIVE_INFINITY;
|
|
10
|
+
for (const line of logTail.split('\n')) {
|
|
11
|
+
const trimmed = line.trim();
|
|
12
|
+
if (!trimmed.startsWith('{') || !trimmed.includes('"sks.desktop_bridge.rejected'))
|
|
13
|
+
continue;
|
|
14
|
+
let record;
|
|
15
|
+
try {
|
|
16
|
+
record = JSON.parse(trimmed);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (record.event !== 'sks.desktop_bridge.rejected' && record.event !== 'sks.desktop_bridge.rejected_summary')
|
|
22
|
+
continue;
|
|
23
|
+
const code = typeof record.code === 'string' ? record.code : '';
|
|
24
|
+
const unreachable = UNREACHABLE_REJECTION_CODE.test(code);
|
|
25
|
+
const rerouted = !unreachable && REROUTED_REJECTION_CODE.test(code);
|
|
26
|
+
if (!unreachable && !rerouted)
|
|
27
|
+
continue;
|
|
28
|
+
const at = typeof record.at === 'string' ? Date.parse(record.at) : Number.NaN;
|
|
29
|
+
if (!Number.isFinite(at) || at < cutoffMs)
|
|
30
|
+
continue;
|
|
31
|
+
if (rerouted) {
|
|
32
|
+
latestReroute = Math.max(latestReroute, at);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!latest || at > latest.at)
|
|
36
|
+
latest = { at, code };
|
|
37
|
+
}
|
|
38
|
+
if (!latest || latest.at <= latestReroute)
|
|
39
|
+
return null;
|
|
40
|
+
return latest.code;
|
|
41
|
+
}
|
|
42
|
+
export const UNREACHABLE_UPSTREAM_RECOVERY_ACTION = 'Run `sks doctor --fix` (or `sks bridge repair`) to restart the Desktop Bridge and re-resolve its upstream address.';
|
|
@@ -6,4 +6,4 @@ if (!root) {
|
|
|
6
6
|
process.stderr.write('sksd-hook-daemon-entrypoint: root argument required\n');
|
|
7
7
|
process.exit(1);
|
|
8
8
|
}
|
|
9
|
-
await startSksdHookDaemon(root, async (name, payload) => evaluateHookPayloadOnce(name, payload, { root }));
|
|
9
|
+
await startSksdHookDaemon(root, async (name, payload) => evaluateHookPayloadOnce(name, payload, { root }), { exitOnRetire: true });
|
|
@@ -6,9 +6,11 @@ import path from 'node:path';
|
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { sha256 } from '../fsx.js';
|
|
9
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
9
10
|
const IDLE_SHUTDOWN_MS = 30 * 60 * 1000;
|
|
10
11
|
const CONNECT_TIMEOUT_MS = 150;
|
|
11
12
|
const REQUEST_TIMEOUT_MS = 5_000;
|
|
13
|
+
export const SKSD_VERSION_MISMATCH_ERROR = 'sksd_version_mismatch';
|
|
12
14
|
export function sksdSocketPath(root) {
|
|
13
15
|
return path.join(sksdRuntimeDir(), `sksd-${projectKey(root)}.sock`);
|
|
14
16
|
}
|
|
@@ -125,7 +127,7 @@ async function removeStaleSocketPath(socketPath) {
|
|
|
125
127
|
throw new Error('unsafe_sksd_socket_path_directory');
|
|
126
128
|
await fsp.rm(socketPath, { force: true });
|
|
127
129
|
}
|
|
128
|
-
export async function startSksdHookDaemon(root, handleHook) {
|
|
130
|
+
export async function startSksdHookDaemon(root, handleHook, options = {}) {
|
|
129
131
|
const socketPath = sksdSocketPath(root);
|
|
130
132
|
const pidFilePath = sksdPidFilePath(root);
|
|
131
133
|
const runtimeDir = path.dirname(socketPath);
|
|
@@ -157,8 +159,13 @@ export async function startSksdHookDaemon(root, handleHook) {
|
|
|
157
159
|
});
|
|
158
160
|
async function respond(line) {
|
|
159
161
|
let response;
|
|
162
|
+
let retire = false;
|
|
160
163
|
try {
|
|
161
164
|
const request = JSON.parse(line);
|
|
165
|
+
if (request.sks_version && request.sks_version !== PACKAGE_VERSION) {
|
|
166
|
+
retire = true;
|
|
167
|
+
throw new Error(SKSD_VERSION_MISMATCH_ERROR);
|
|
168
|
+
}
|
|
162
169
|
const result = await handleHook(request.name, request.payload);
|
|
163
170
|
response = { schema: 'sks.sksd-hook-response.v1', ok: true, result };
|
|
164
171
|
}
|
|
@@ -170,6 +177,11 @@ export async function startSksdHookDaemon(root, handleHook) {
|
|
|
170
177
|
}
|
|
171
178
|
catch { }
|
|
172
179
|
socket.end();
|
|
180
|
+
if (retire) {
|
|
181
|
+
void close().then(() => { if (options.exitOnRetire)
|
|
182
|
+
process.exit(0); });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
173
185
|
resetIdleTimer();
|
|
174
186
|
}
|
|
175
187
|
socket.on('error', () => undefined);
|
|
@@ -270,7 +282,7 @@ export async function callSksdHookDaemon(root, name, payload) {
|
|
|
270
282
|
let buffer = '';
|
|
271
283
|
socket.on('connect', () => {
|
|
272
284
|
clearTimeout(connectTimer);
|
|
273
|
-
const request = { schema: 'sks.sksd-hook-request.v1', name, payload };
|
|
285
|
+
const request = { schema: 'sks.sksd-hook-request.v1', name, payload, sks_version: PACKAGE_VERSION };
|
|
274
286
|
socket.write(`${JSON.stringify(request)}\n`);
|
|
275
287
|
});
|
|
276
288
|
socket.on('data', (chunk) => {
|
|
@@ -4,48 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
import { desktopBridgeStatusV3, executeDesktopBridgeCommandV3 } from '../codex-lb/desktop-controller-v3.js';
|
|
5
5
|
import { bootstrapExistingDesktopBridgeService, desktopBridgeServicePaths, desktopBridgeServiceStatus } from '../codex-lb/desktop-service.js';
|
|
6
6
|
import { desktopBridgeRuntimeVersion, desktopBridgeRuntimeVersionStale, readDesktopBridgeState } from '../codex-lb/desktop-bridge/state.js';
|
|
7
|
+
import { BRIDGE_LOG_TAIL_BYTES, detectUnreachableUpstreamEvidence } from '../codex-lb/desktop-bridge/upstream-evidence.js';
|
|
7
8
|
import { PACKAGE_VERSION } from '../version.js';
|
|
8
|
-
export
|
|
9
|
-
const BRIDGE_LOG_TAIL_BYTES = 256 * 1024;
|
|
10
|
-
const UNREACHABLE_REJECTION_CODE = /^bridge_(?:websocket_)?upstream_unavailable/;
|
|
11
|
-
const REROUTED_REJECTION_CODE = /^bridge_upstream_unreachable_rerouted/;
|
|
12
|
-
export function detectUnreachableUpstreamEvidence(logTail, startedAt, nowMs) {
|
|
13
|
-
const startedMs = startedAt ? Date.parse(startedAt) : Number.NaN;
|
|
14
|
-
const cutoffMs = Math.max(Number.isFinite(startedMs) ? startedMs : 0, nowMs - BRIDGE_UNREACHABLE_EVIDENCE_WINDOW_MS);
|
|
15
|
-
let latest = null;
|
|
16
|
-
let latestReroute = Number.NEGATIVE_INFINITY;
|
|
17
|
-
for (const line of logTail.split('\n')) {
|
|
18
|
-
const trimmed = line.trim();
|
|
19
|
-
if (!trimmed.startsWith('{') || !trimmed.includes('"sks.desktop_bridge.rejected'))
|
|
20
|
-
continue;
|
|
21
|
-
let record;
|
|
22
|
-
try {
|
|
23
|
-
record = JSON.parse(trimmed);
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (record.event !== 'sks.desktop_bridge.rejected' && record.event !== 'sks.desktop_bridge.rejected_summary')
|
|
29
|
-
continue;
|
|
30
|
-
const code = typeof record.code === 'string' ? record.code : '';
|
|
31
|
-
const unreachable = UNREACHABLE_REJECTION_CODE.test(code);
|
|
32
|
-
const rerouted = !unreachable && REROUTED_REJECTION_CODE.test(code);
|
|
33
|
-
if (!unreachable && !rerouted)
|
|
34
|
-
continue;
|
|
35
|
-
const at = typeof record.at === 'string' ? Date.parse(record.at) : Number.NaN;
|
|
36
|
-
if (!Number.isFinite(at) || at < cutoffMs)
|
|
37
|
-
continue;
|
|
38
|
-
if (rerouted) {
|
|
39
|
-
latestReroute = Math.max(latestReroute, at);
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (!latest || at > latest.at)
|
|
43
|
-
latest = { at, code };
|
|
44
|
-
}
|
|
45
|
-
if (!latest || latest.at <= latestReroute)
|
|
46
|
-
return null;
|
|
47
|
-
return latest.code;
|
|
48
|
-
}
|
|
9
|
+
export { BRIDGE_UNREACHABLE_EVIDENCE_WINDOW_MS, detectUnreachableUpstreamEvidence } from '../codex-lb/desktop-bridge/upstream-evidence.js';
|
|
49
10
|
export async function readDesktopBridgeUnreachableUpstreamEvidence(home, deps = {}) {
|
|
50
11
|
const paths = desktopBridgeServicePaths(home);
|
|
51
12
|
const state = await readDesktopBridgeState(paths.state_path).catch(() => null);
|
|
@@ -142,14 +103,15 @@ export async function repairDoctorDesktopBridgeCatalog(input, deps = {}) {
|
|
|
142
103
|
const status = await statusImpl({ home: bridgeHome, env: process.env });
|
|
143
104
|
const blockers = (status?.readiness?.blockers || []).map(String);
|
|
144
105
|
const stale = blockers.filter((blocker) => blocker.endsWith('_catalog_stale'));
|
|
106
|
+
const reverify = { fix: input.fix, bridgeHome, restarted: restarted.restarted };
|
|
145
107
|
if (!status?.management?.managed || stale.length === 0) {
|
|
146
|
-
return {
|
|
108
|
+
return reverifyTransportIfDegraded({
|
|
147
109
|
...base,
|
|
148
110
|
ok: restarted.blockers.length === 0,
|
|
149
111
|
repaired: restarted.restarted,
|
|
150
112
|
warnings: restarted.warnings,
|
|
151
113
|
blockers: restarted.blockers
|
|
152
|
-
};
|
|
114
|
+
}, reverify, statusImpl, executeImpl);
|
|
153
115
|
}
|
|
154
116
|
const attempts = [];
|
|
155
117
|
let syncedAndVerified = false;
|
|
@@ -165,7 +127,7 @@ export async function repairDoctorDesktopBridgeCatalog(input, deps = {}) {
|
|
|
165
127
|
}
|
|
166
128
|
attempts.push(`catalog_sync_attempt_${attempt}:${commandOk ? `still_stale:${stillStale.join(',')}` : (sync?.execution?.blockers || ['failed']).map(String).join(',')}`);
|
|
167
129
|
}
|
|
168
|
-
return {
|
|
130
|
+
return reverifyTransportIfDegraded({
|
|
169
131
|
...base,
|
|
170
132
|
ok: syncedAndVerified && restarted.blockers.length === 0,
|
|
171
133
|
repaired: syncedAndVerified || restarted.restarted,
|
|
@@ -174,9 +136,29 @@ export async function repairDoctorDesktopBridgeCatalog(input, deps = {}) {
|
|
|
174
136
|
...(syncedAndVerified ? [] : ['desktop_bridge_catalog_still_stale_after_repair', ...attempts]),
|
|
175
137
|
...restarted.blockers
|
|
176
138
|
]
|
|
177
|
-
};
|
|
139
|
+
}, reverify, statusImpl, executeImpl);
|
|
178
140
|
}
|
|
179
141
|
catch (error) {
|
|
180
142
|
return { ...base, ok: false, repaired: false, blockers: [String(error?.message || 'desktop_bridge_catalog_sync_failed')] };
|
|
181
143
|
}
|
|
182
144
|
}
|
|
145
|
+
async function reverifyTransportIfDegraded(result, input, statusImpl, executeImpl) {
|
|
146
|
+
if (!input.fix)
|
|
147
|
+
return result;
|
|
148
|
+
const before = await statusImpl({ home: input.bridgeHome, env: process.env }).catch(() => null);
|
|
149
|
+
const serving = before?.service?.state === 'ready' || before?.service?.running === true;
|
|
150
|
+
const degraded = before?.readiness?.state === 'degraded';
|
|
151
|
+
if (before?.management?.managed !== true || !serving || (!degraded && !input.restarted))
|
|
152
|
+
return result;
|
|
153
|
+
await executeImpl({ operation: 'verify', level: 'transport' }, { home: input.bridgeHome, env: process.env }).catch(() => null);
|
|
154
|
+
const after = await statusImpl({ home: input.bridgeHome, env: process.env }).catch(() => null);
|
|
155
|
+
const afterState = String(after?.readiness?.state || 'unknown');
|
|
156
|
+
return {
|
|
157
|
+
...result,
|
|
158
|
+
repaired: result.repaired || (degraded && afterState === 'ready'),
|
|
159
|
+
warnings: [
|
|
160
|
+
...result.warnings,
|
|
161
|
+
afterState === 'ready' ? 'desktop_bridge_transport_reverified' : `desktop_bridge_transport_reverify_incomplete:${afterState}`
|
|
162
|
+
]
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { manualProofRoutesBlockReadiness } from '../verification-profile.js';
|
|
3
4
|
export const DOCTOR_READINESS_MATRIX_SCHEMA = 'sks.doctor-readiness-matrix.v2';
|
|
4
5
|
const OPTIONAL_ROUTE_SCOPES = new Set(['route-computer-use', 'route-chrome-web-review', 'route-app-handoff', 'route-app-screenshot']);
|
|
6
|
+
const MANUAL_PROOF_ROUTE_SCOPES = new Set(['route-image']);
|
|
7
|
+
function optionalRouteScope(scope) {
|
|
8
|
+
if (OPTIONAL_ROUTE_SCOPES.has(scope))
|
|
9
|
+
return true;
|
|
10
|
+
return MANUAL_PROOF_ROUTE_SCOPES.has(scope) && !manualProofRoutesBlockReadiness();
|
|
11
|
+
}
|
|
12
|
+
function desktopBridgeReadinessView(value) {
|
|
13
|
+
if (!value || typeof value !== 'object')
|
|
14
|
+
return null;
|
|
15
|
+
if (value.management || value.readiness)
|
|
16
|
+
return value;
|
|
17
|
+
const status = value.status;
|
|
18
|
+
return status && typeof status === 'object' && (status.management || status.readiness) ? status : value;
|
|
19
|
+
}
|
|
5
20
|
export async function writeDoctorReadinessMatrix(root, input = {}) {
|
|
6
21
|
const matrix = buildDoctorReadinessMatrix(input);
|
|
7
22
|
const reportPath = input.reportPath || path.join(root, '.sneakoscope', 'reports', 'doctor-ready-breakdown.json');
|
|
@@ -76,13 +91,17 @@ export function buildDoctorReadinessMatrix(input = {}) {
|
|
|
76
91
|
if (codexAppHarness?.ok === false)
|
|
77
92
|
for (const blocker of normalizeList(codexAppHarness.blockers))
|
|
78
93
|
warnings.add(`codex_app_harness:${blocker}`);
|
|
79
|
-
const desktopBridge = input.desktop_bridge_status || input.desktop_bridge || null;
|
|
94
|
+
const desktopBridge = desktopBridgeReadinessView(input.desktop_bridge_status || input.desktop_bridge || null);
|
|
80
95
|
if (desktopBridge?.management?.managed === true && desktopBridge?.readiness?.ready !== true) {
|
|
81
96
|
const bridgeBlockers = normalizeList(desktopBridge?.readiness?.blockers);
|
|
82
97
|
for (const blocker of bridgeBlockers)
|
|
83
98
|
blockers.add(blocker);
|
|
84
|
-
if (!bridgeBlockers.length)
|
|
85
|
-
|
|
99
|
+
if (!bridgeBlockers.length) {
|
|
100
|
+
if (desktopBridge?.readiness?.state === 'degraded')
|
|
101
|
+
warnings.add('desktop_bridge_readiness_degraded:transport_unverified_for_current_process');
|
|
102
|
+
else
|
|
103
|
+
blockers.add('desktop_bridge_not_ready');
|
|
104
|
+
}
|
|
86
105
|
}
|
|
87
106
|
if (desktopBridge?.management?.managed === true && desktopBridge?.service?.running !== true) {
|
|
88
107
|
warnings.add('desktop_bridge_service_not_running');
|
|
@@ -136,7 +155,7 @@ export function buildDoctorReadinessMatrix(input = {}) {
|
|
|
136
155
|
for (const [scope, list] of Object.entries(routeBlockers)) {
|
|
137
156
|
for (const blocker of Array.isArray(list) ? list : []) {
|
|
138
157
|
const value = `route:${scope}:${String(blocker)}`;
|
|
139
|
-
if (
|
|
158
|
+
if (optionalRouteScope(String(scope)))
|
|
140
159
|
warnings.add(value);
|
|
141
160
|
else
|
|
142
161
|
blockers.add(value);
|
|
@@ -6,6 +6,7 @@ import { allowlistedManagedRouteSkillNames, dollarCommand, INVALID_EXPLICIT_MANA
|
|
|
6
6
|
import { inspectConfinedPath } from '../managed-path-safety.js';
|
|
7
7
|
import { renderAuthoritativeSksSkillContext } from '../codex-native/sks-skill-paths.js';
|
|
8
8
|
import { resolveManagedSkillSourcesForAdmission } from './managed-skill-admission.js';
|
|
9
|
+
import { managedSkillDigestBlocksEnforced } from '../verification-profile.js';
|
|
9
10
|
import { looksLikeActiveContinuationPrompt } from './naruto-decision-gate.js';
|
|
10
11
|
import { extractUserPrompt, looksLikeCodexGitAction, looksLikeCodexUiSettingsEvent } from './payload-signals.js';
|
|
11
12
|
const STANDALONE_PARENT_BASE_SKILLS = [
|
|
@@ -59,7 +60,7 @@ export async function attachAuthoritativeSksSkillContext(root, state, payload, r
|
|
|
59
60
|
return result;
|
|
60
61
|
const admission = await authoritativeSksSkillAdmission(root, skillNames);
|
|
61
62
|
if (admission.blocked)
|
|
62
|
-
return { ...result, ...admission.blocked };
|
|
63
|
+
return managedSkillDigestBlocksEnforced(root) ? { ...result, ...admission.blocked } : result;
|
|
63
64
|
const resolution = admission.resolution;
|
|
64
65
|
if (!resolution)
|
|
65
66
|
return result;
|
|
@@ -211,7 +212,9 @@ export async function hookActiveSkillContextRefresh(root, state, name) {
|
|
|
211
212
|
return {
|
|
212
213
|
continue: true,
|
|
213
214
|
...(spawnCompatibility ? { additionalContext: spawnCompatibility } : {}),
|
|
214
|
-
systemMessage:
|
|
215
|
+
systemMessage: managedSkillDigestBlocksEnforced(root)
|
|
216
|
+
? 'SKS managed skill refresh could not verify the current installation. Do not use a stale skill location; the next active tool call will be denied until the installation is repaired.'
|
|
217
|
+
: 'SKS managed skill refresh could not verify the current installation; continuing without unverified skill context. `sks doctor --fix` repairs the installation when convenient.'
|
|
215
218
|
};
|
|
216
219
|
}
|
|
217
220
|
const additionalContext = [spawnCompatibility, refresh.context].filter(Boolean).join('\n\n');
|
|
@@ -10,6 +10,7 @@ import { isMadSksRouteState } from './permission-gates.js';
|
|
|
10
10
|
import { classifyMadSksShellCommand } from './mad-sks/write-guard.js';
|
|
11
11
|
import { activeRouteContext, evaluateStop, prepareRoute, promptPipelineContext as routePipelineContext, recordContext7Evidence, recordSubagentEvidence, routePrompt } from './pipeline.js';
|
|
12
12
|
import { localizedFinalizationReason } from './language-preference.js';
|
|
13
|
+
import { managedSkillDigestBlocksEnforced, postToolEvidenceEnabled, stopFinalizationRitualsEnforced } from './verification-profile.js';
|
|
13
14
|
import { classifyToolError } from './evaluation.js';
|
|
14
15
|
import { dollarCommand, managedSkillNamesForPrompt, stripVisibleDecisionAnswerBlocks } from './routes.js';
|
|
15
16
|
import { coreEngineeringDirectiveReferenceText } from './lean-engineering-policy.js';
|
|
@@ -250,19 +251,19 @@ async function hookUserPrompt(root, state, payload, noQuestion, sessionKey = nul
|
|
|
250
251
|
}).catch(() => toolOutputQuarantine);
|
|
251
252
|
}
|
|
252
253
|
if (submittedPrompt && (detectedMissingCallId || toolOutputQuarantine)) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
};
|
|
254
|
+
const reason = interruptedToolOutputRecoveryBlockReason({
|
|
255
|
+
callId: detectedMissingCallId || toolOutputQuarantine?.call_id,
|
|
256
|
+
missionId: state?.mission_id || toolOutputQuarantine?.mission_id
|
|
257
|
+
});
|
|
258
|
+
if (stopFinalizationRitualsEnforced(root))
|
|
259
|
+
return { decision: 'block', reason };
|
|
260
|
+
return { continue: true, additionalContext: reason, systemMessage: visibleHookMessage('user-prompt-submit', reason) };
|
|
260
261
|
}
|
|
261
262
|
const parentLaunchMissionId = activeNarutoParentLaunchMissionId();
|
|
262
263
|
if (parentLaunchMissionId) {
|
|
263
264
|
const parentSkillNames = await standaloneParentManagedSkillNames(root, parentLaunchMissionId, state);
|
|
264
265
|
const skillAdmission = await authoritativeSksSkillAdmission(root, parentSkillNames);
|
|
265
|
-
if (skillAdmission.blocked)
|
|
266
|
+
if (skillAdmission.blocked && managedSkillDigestBlocksEnforced(root))
|
|
266
267
|
return skillAdmission.blocked;
|
|
267
268
|
const parentHostCapability = await claimStandaloneParentHostCapabilityRuntime({
|
|
268
269
|
root,
|
|
@@ -339,7 +340,7 @@ async function hookUserPrompt(root, state, payload, noQuestion, sessionKey = nul
|
|
|
339
340
|
}
|
|
340
341
|
if (!explicitCommand && !clarificationPending && !madConfirmationPrompt && !activeContinuation && lightRoute?.id === 'Answer') {
|
|
341
342
|
const skillAdmission = await authoritativeSksSkillAdmission(root, lightRoute.requiredSkills || ['answer', 'honest-mode']);
|
|
342
|
-
if (skillAdmission.blocked)
|
|
343
|
+
if (skillAdmission.blocked && managedSkillDigestBlocksEnforced(root))
|
|
343
344
|
return skillAdmission.blocked;
|
|
344
345
|
const turnId = hookTurnId(payload);
|
|
345
346
|
if (turnId) {
|
|
@@ -413,7 +414,7 @@ async function hookUserPrompt(root, state, payload, noQuestion, sessionKey = nul
|
|
|
413
414
|
}
|
|
414
415
|
if (!explicitActiveWorkflowReplacement) {
|
|
415
416
|
const activeSkillAdmission = await authoritativeSksSkillAdmission(root, selectedSksSkillNamesForActiveState(state));
|
|
416
|
-
if (activeSkillAdmission.blocked)
|
|
417
|
+
if (activeSkillAdmission.blocked && managedSkillDigestBlocksEnforced(root))
|
|
417
418
|
return activeSkillAdmission.blocked;
|
|
418
419
|
const queued = await queueActiveOfficialWorkflowPrompt({
|
|
419
420
|
root,
|
|
@@ -443,7 +444,7 @@ async function hookUserPrompt(root, state, payload, noQuestion, sessionKey = nul
|
|
|
443
444
|
}
|
|
444
445
|
}
|
|
445
446
|
const skillAdmission = await authoritativeSksSkillAdmission(root, managedSkillNamesForPrompt(route, prompt));
|
|
446
|
-
if (skillAdmission.blocked)
|
|
447
|
+
if (skillAdmission.blocked && managedSkillDigestBlocksEnforced(root))
|
|
447
448
|
return skillAdmission.blocked;
|
|
448
449
|
const skillContext = skillAdmission.resolution
|
|
449
450
|
? renderAuthoritativeSksSkillContext(skillAdmission.resolution)
|
|
@@ -594,11 +595,12 @@ async function hookPreTool(root, state, payload, noQuestion, sessionKey = null)
|
|
|
594
595
|
skillAvailabilityBlock = await evaluateSkillAvailabilityBlock();
|
|
595
596
|
}
|
|
596
597
|
}
|
|
597
|
-
|
|
598
|
+
const digestBlocks = managedSkillDigestBlocksEnforced(root);
|
|
599
|
+
if (skillAvailabilityBlock && digestBlocks) {
|
|
598
600
|
return { decision: 'block', permissionDecision: 'deny', reason: skillAvailabilityBlock };
|
|
599
601
|
}
|
|
600
602
|
const skillRefresh = await activeAuthoritativeSksSkillRefresh(root, state);
|
|
601
|
-
if (skillRefresh.blocked) {
|
|
603
|
+
if (skillRefresh.blocked && digestBlocks) {
|
|
602
604
|
return {
|
|
603
605
|
decision: 'block',
|
|
604
606
|
permissionDecision: 'deny',
|
|
@@ -667,12 +669,13 @@ async function parentWaveGuidanceContext(root, state = {}, sessionKey = null) {
|
|
|
667
669
|
}
|
|
668
670
|
async function hookPostTool(root, state, payload, noQuestion, sessionKey = null) {
|
|
669
671
|
state = { ...state, _session_key: state?._session_key || sessionKey };
|
|
672
|
+
const evidence = postToolEvidenceEnabled(root);
|
|
670
673
|
await Promise.all([
|
|
671
674
|
recordHostCapabilityPostTool(root, state, payload, sessionKey).catch(() => null),
|
|
672
675
|
recordMadSksSqlPlanePostToolLifecycle(root, state, payload).catch(() => null),
|
|
673
|
-
recordContext7Evidence(root, state, payload).catch(() => null),
|
|
674
|
-
recordSubagentEvidence(root, state, payload).catch(() => null),
|
|
675
|
-
toolFailed(payload) ? recordToolErrorTaxonomy(root, state, payload).catch(() => null) : Promise.resolve(null)
|
|
676
|
+
evidence ? recordContext7Evidence(root, state, payload).catch(() => null) : Promise.resolve(null),
|
|
677
|
+
evidence ? recordSubagentEvidence(root, state, payload).catch(() => null) : Promise.resolve(null),
|
|
678
|
+
evidence && toolFailed(payload) ? recordToolErrorTaxonomy(root, state, payload).catch(() => null) : Promise.resolve(null)
|
|
676
679
|
]);
|
|
677
680
|
if (!noQuestion)
|
|
678
681
|
return { continue: true };
|
|
@@ -1029,6 +1032,9 @@ async function hookStop(root, state, payload, noQuestion, sessionKey = null) {
|
|
|
1029
1032
|
if (state?.subagents_required === true) {
|
|
1030
1033
|
await refreshOfficialSubagentCompletionArtifacts(root, state, last, sessionKey).catch(() => null);
|
|
1031
1034
|
}
|
|
1035
|
+
if (!noQuestion && !stopFinalizationRitualsEnforced(root)) {
|
|
1036
|
+
return { continue: true, action: 'essential_profile_stop_accepted' };
|
|
1037
|
+
}
|
|
1032
1038
|
const routeDecision = await evaluateStop(root, state, payload, { noQuestion });
|
|
1033
1039
|
if (routeDecision && !successfulAppNarutoStopNeedsVisibleSummary(state, routeDecision)) {
|
|
1034
1040
|
return routeDecision;
|
package/dist/core/init/skills.js
CHANGED
|
@@ -38,7 +38,7 @@ async function installOfficialSkills(root) {
|
|
|
38
38
|
.trim();
|
|
39
39
|
const skills = {
|
|
40
40
|
'dfix': `---\nname: dfix\ndescription: Direct Fix mode for $DFix or $dfix requests and inferred tiny copy/config/docs/labels/spacing/translation/simple mechanical edits.\n---\n\nUse for tiny copy/config/docs/labels/spacing/translation/simple mechanical edits. List exact micro-edits, inspect only needed files, apply only those edits, and run cheap verification. Keep broad implementation routed to Naruto; for UI/UX micro-edits read \`design.md\` when present and use imagegen for image/logo/raster assets. Bypass broad SKS routing, mission state, TriWiki/TriFix/reflection/state recording, Goal, Research, eval, redesign, and repeated full-route Honest Mode loops. Start the final answer with \`DFix 완료 요약:\` and include one \`DFix 솔직모드:\` line covering verified, not verified, and remaining issues. ${CODEX_IMAGEGEN_REQUIRED_POLICY}\n`,
|
|
41
|
-
'answer': `---\nname: answer\ndescription: Answer questions with research or docs; never implement.\n---\n\nUse for explanations, comparisons, status, facts, source-backed research, or docs guidance. Use repo/TriWiki first for project-local facts; hydrate low-trust claims from source. Browse or use Context7 for current external package/API/framework/MCP docs. End with a concise answer
|
|
41
|
+
'answer': `---\nname: answer\ndescription: Answer questions with research or docs; never implement.\n---\n\nUse for explanations, comparisons, status, facts, source-backed research, or docs guidance. Use repo/TriWiki first for project-local facts; hydrate low-trust claims from source. Browse or use Context7 for current external package/API/framework/MCP docs. End with a concise answer that separates what was verified from what was inferred; do not create missions, subagents, or file edits.\n`,
|
|
42
42
|
'sks': `---\nname: sks\ndescription: General Sneakoscope Codex command route for $SKS or $sks usage, setup, status, and workflow help.\n---\n\nUse local SKS commands: bootstrap, deps, commands, quickstart, codex-app, context7, guard, conflicts, reasoning, wiki, pipeline status, pipeline plan, skill-dream. Promote code-changing work to Naruto unless Answer/DFix/Help/Wiki/safety route fits. Surface route/guard/scope, use TriWiki, do not edit installed harness files outside this engine repo, and clear conflicting third-party Codex harness markers via \`sks conflicts cleanup --yes\`. ${skillDreamPolicyText()}\n`,
|
|
43
43
|
'plan': `---\nname: plan\ndescription: Write a plan artifact only; never edit product or source files.\n---\n\nUse when the user invokes $Plan or asks for a plan-only frontdoor. Produce a concrete plan artifact under .sneakoscope/plans/<slug>.md with goal, scope, files to inspect, implementation steps, acceptance checks, and rollback notes. Do not edit product/source files, generated harness files, package metadata, or docs beyond the plan artifact. Keep implementation_allowed=false and hand off execution to $Work only after the user or route explicitly moves from planning to work. Finish with what is planned, what remains unimplemented, and Honest Mode.\n`,
|
|
44
44
|
'work': `---\nname: work\ndescription: Execute the latest plan with evidence-gated completion. 예: $Work\n---\n\nUse when the user invokes $Work or asks to execute the latest SKS plan. Resolve the newest .sneakoscope/plans/*.md, route execution through Naruto evidence gates, keep leases and verification artifacts current, and do not claim completion without machine evidence or explicit blocker evidence. If no plan exists, block with a clear next action: run $Plan first or provide a task.\n`,
|
|
@@ -52,7 +52,7 @@ async function installOfficialSkills(root) {
|
|
|
52
52
|
'from-chat-img': `---\nname: from-chat-img\ndescription: Explicit $From-Chat-IMG Naruto add-on gate for chat screenshot plus attachment analysis.\n---\n\nUse only for From-Chat-IMG/$From-Chat-IMG. It enters the Naruto pipeline with from_chat_img_required=true and an add-on coverage gate. Treat uploads as chat screenshot plus originals. For web/browser/webapp targets use Codex Chrome Extension first; for native Mac/non-web app surfaces use Codex Computer Use visual inspection when available. List requirements first, match regions to attachments with confidence, write ${FROM_CHAT_IMG_COVERAGE_ARTIFACT}, ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT}, ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT}, and ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT}, then continue Naruto worker proof, review, reflection, and Honest Mode. ${CODEX_WEB_VERIFICATION_POLICY} ${CODEX_COMPUTER_USE_ONLY_POLICY} The ledger must account for every visible customer request, screenshot image region, and separate attachment; ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT} must have a checked item for each request, image-region/attachment match, work item, scoped QA-LOOP, and verification step; ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT} stores temporary TriWiki-backed session context with expires_after_sessions=${FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS}. ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT} must prove QA-LOOP ran over the exact customer-request work-order range after implementation, with every work item covered, post-fix verification complete, and zero unresolved findings. naruto-gate.json cannot pass From-Chat-IMG completion until from_chat_img_request_coverage=true, unresolved_items is empty, every checklist box is checked, and scoped_qa_loop_completed=true.\n`,
|
|
53
53
|
'qa-loop': `---\nname: qa-loop\ndescription: $QA-LOOP dogfoods UI/API as human proxy with safety gates, Codex Chrome Extension-first web UI evidence, safe fixes, rechecks, and a QA report.\n---\n\nUse only $QA-LOOP. Infer scope, target, mutation policy, and login boundary from the prompt plus TriWiki/current-code defaults; do not surface a prequestion sheet. Credentials are runtime-only; never save secrets. Web/browser/webapp UI-level E2E must run the Codex Chrome Extension readiness gate first; if the extension is missing or disabled, rapidly halt and ask the user to set it up, then resume only after the user confirms installation is complete. Codex Computer Use is reserved for native Mac/non-web surfaces and must not satisfy web UI evidence. Playwright, Selenium, Puppeteer, Browser Use, Chrome MCP, screenshots fabricated from code, and prose-only checks do not satisfy web UI/browser verification. ${CODEX_WEB_VERIFICATION_POLICY} Deployed targets are read-only; destructive removal is forbidden. After answer/run, dogfood real flows, apply safe contract-allowed code/test/docs fixes, recheck, and do not pass qa-gate.json with unresolved findings or without post_fix_verification_complete. Finish qa-ledger, date/version report, gate, completion summary, and Honest Mode.\n`,
|
|
54
54
|
'ppt': `---\nname: ppt\ndescription: $PPT information-first HTML/PDF presentation pipeline with inferred STP, audience, pain-point, format, research, design-system, and verification contract.\n---\n\nUse only when the user invokes $PPT or asks to create a presentation, deck, slides, pitch deck, proposal deck, HTML presentation, or PDF presentation artifact. Before artifact work, auto-seal presentation-specific answers from prompt, TriWiki/current-code defaults, and conservative policy: delivery context, target audience profile including role/average age/job/industry/topic familiarity/decision power, STP strategy, decision context and objections, and 3+ pain-point to solution mappings with expected aha moments. Do not surface a prequestion sheet. Presentation design must be simple, restrained, and information-first: avoid over-designed decoration, ornamental gradients, nested cards, and effects that compete with the message. Design detail should be embedded through typography hierarchy, spacing, alignment, thin rules, source clarity, and subtle accents. ${pptPipelineAllowlistPolicyText()} Use Product Design plugin first for context, ideation, prototype direction, audit, design QA, and share handoff. Use design.md only as an existing project-local cache or fallback SSOT when Product Design is unavailable; if fallback creation is needed, use docs/Design-Sys-Prompt.md plus getdesign-reference and curated DESIGN.md examples from ${AWESOME_DESIGN_MD_REFERENCE.url} only as source inputs, then fuse them into route-local PPT style tokens with a recorded design_ssot instead of treating references as parallel authorities. The $PPT route always loads imagegen as a required skill, checks Codex App imagegen at route start, and runs SKS auto-repair once before any image-dependent build/review work. If repair fails, stop with codex_imagegen_unavailable and do not continue with image-free or API-substituted evidence. When the sealed contract needs a generated raster asset or generated slide visual critique, immediately invoke Codex App \`$imagegen\` with gpt-image-2, move/copy the selected output into the mission assets or review evidence path, and record the real file path in ppt-image-asset-ledger.json or ppt-review-ledger.json before building or passing the gate. Direct API fallback, placeholder files, HTML/CSS stand-ins, and prose-only substitutes do not satisfy the route gate. ${productDesignPluginPolicyText()} ${CODEX_IMAGEGEN_REQUIRED_POLICY} Use web or Context7 evidence only when external facts/libraries/current docs are required by the PPT contract, record verified claims in ppt-fact-ledger.json, record generated image asset plans/results/blockers in ppt-image-asset-ledger.json, then create the PDF plus editable source HTML under source-html/, keep independent strategy/render/file-write phases parallel where inputs allow, record ppt-parallel-report.json, run the bounded ppt-review-policy/ppt-review-ledger/ppt-iteration-report loop, and verify readability, overlap, format fit, source coverage, export state, unsupported-claim status, image-asset completion, review-loop termination, and temporary build files cleanup. Finish with reflection and Honest Mode.\n`,
|
|
55
|
-
'computer-use-fast': `---\nname: computer-use-fast\ndescription: Alias for the maximum-speed $Computer-Use/$CU native Codex Computer Use lane.\n---\n\nUse the same rules as computer-use: skip Naruto delegation, QA-LOOP clarification, upfront TriWiki refresh, Context7, subagents, and reflection unless explicitly requested. Use Codex Computer Use directly only for native macOS, desktop-app, OS-settings, or non-web visual tasks. Browser, localhost, website, webapp, and web-based app verification must use the Codex Chrome Extension path first and must halt if that extension is not installed/enabled. At the end only, refresh/pack TriWiki, validate it, then provide a concise completion summary
|
|
55
|
+
'computer-use-fast': `---\nname: computer-use-fast\ndescription: Alias for the maximum-speed $Computer-Use/$CU native Codex Computer Use lane.\n---\n\nUse the same rules as computer-use: skip Naruto delegation, QA-LOOP clarification, upfront TriWiki refresh, Context7, subagents, and reflection unless explicitly requested. Use Codex Computer Use directly only for native macOS, desktop-app, OS-settings, or non-web visual tasks. Browser, localhost, website, webapp, and web-based app verification must use the Codex Chrome Extension path first and must halt if that extension is not installed/enabled. At the end only, refresh/pack TriWiki, validate it, then provide a concise completion summary that separates what was verified from what was not. ${CODEX_WEB_VERIFICATION_POLICY} ${CODEX_COMPUTER_USE_ONLY_POLICY}\n`,
|
|
56
56
|
'cu': `---\nname: cu\ndescription: Short alias for the maximum-speed native $Computer-Use Codex Computer Use lane.\n---\n\nUse the same rules as computer-use. This is a speed lane for native macOS, desktop-app, OS-settings, and non-web visual tasks requiring Codex Computer Use evidence, with TriWiki refresh/validate and Honest Mode deferred to final closeout. Web/browser/webapp verification must use Codex Chrome Extension first and stop if the extension is not installed/enabled. ${CODEX_WEB_VERIFICATION_POLICY} ${CODEX_COMPUTER_USE_ONLY_POLICY}\n`,
|
|
57
57
|
'goal': `---\nname: goal\ndescription: Codex-native $Goal/$goal control with detailed completion criteria and no SKS-owned Goal state.\n---\n\nUse when the user invokes $Goal/$goal or asks to create, edit, view, pause, resume, or clear a persisted goal. Use only Codex native Goal controls or the callable native goal tool. Do not run sks goal, create an SKS mission, write bridge artifacts, compile a compatibility loop, create fallback Goal state, refresh TriWiki, or open subagents for the Goal control turn. Before create or edit, expand the request into explicit Outcome, Scope, Constraints, Verification, Done when, Stop conditions, and Non-goals. Completion criteria must be measurable and must stop work once satisfied; forbid unrelated refactors, speculative expansion, and open-ended polishing. If native Goal is unavailable, report that limitation instead of substituting an SKS implementation.\n`,
|
|
58
58
|
'release-review': `---\nname: release-review\ndescription: Review release readiness through official Codex subagents.\n---\n\nUse only when the user invokes $Release-Review or asks for a release-readiness review. Run it through \`sks naruto run \"$Release-Review release audit\" --agents <n> --read-only --json\` or the current Codex parent session. The parent owns decomposition, waits for every official worker/expert thread, integrates one structured outcome per thread, and records subagent-plan.json, subagent-events.jsonl, subagent-parent-summary.json, and subagent-evidence.json. No alternate Release-Review runtime is supported. Finish with release-readiness verification and Honest Mode.\n`,
|
|
@@ -67,13 +67,13 @@ async function installOfficialSkills(root) {
|
|
|
67
67
|
'prompt-pipeline': `---\nname: prompt-pipeline\ndescription: Default SKS prompt routing for execution requests; Answer and DFix remain lightweight.\n---\n\nClassify only the route and the constraints that change execution. Answer handles real information requests; DFix handles tiny copy/config/docs/labels/spacing/translation/mechanical edits; general code-changing work uses Naruto with Codex official subagents. Preserve the literal user request; TriWiki may enrich it but never replace an explicit requirement. ${coreEngineeringDirectiveReferenceText()} ${engineeringSanityPolicyText()} For code work, identify the requested outcome, actual code/data flow, exact write scope, acceptance criteria, and the smallest meaningful verification. Load route-specific Design, PPT, image, browser, research, DB, release, or Context7 rules only when that route or external contract requires them. Materialize pipeline-plan.json with route, scopes, verification, and blockers, then finish the requested work instead of stopping at the plan.\n`,
|
|
68
68
|
[SOLUTION_SCOUT_SKILL_NAME]: `---\nname: ${SOLUTION_SCOUT_SKILL_NAME}\ndescription: Optional source scout for fixes that materially depend on external precedent.\n---\n\nUse only when the user asks for similar cases or when a package, API, platform, or externally documented behavior makes outside precedent materially useful. Search with the concrete symptom and current stack, prefer primary sources, and keep the local code path authoritative. Do not add this step to ordinary local bugfixes. If external evidence is unavailable, record that gap instead of inventing a workaround.\n`,
|
|
69
69
|
'reasoning-router': `---\nname: reasoning-router\ndescription: Temporary SKS reasoning-effort routing for every command and pipeline route on the GPT-5.6 family.\n---\n\nGPT-5.6 efforts: none/low/medium/high/xhigh/max. Route medium for simple copy/color/discovery/setup/mechanical edits; high for logic, safety, architecture, DB, orchestration, refactor, multi-file work; xhigh for research, AutoResearch, falsification, benchmarks, SEO/GEO, open-ended discovery, and From-Chat-IMG analysis; max only for hardest quality-first judgment. Prefer gpt-5.6-luna / terra / sol role defaults over legacy model aliases. Routing is temporary; return to default after the gate. Inspect with sks reasoning and sks pipeline status.\n`,
|
|
70
|
-
'pipeline-runner': `---\nname: pipeline-runner\ndescription: Execute SKS routes with bounded state
|
|
70
|
+
'pipeline-runner': `---\nname: pipeline-runner\ndescription: Execute SKS routes with bounded state and evidence.\n---\n\nEvery $ command selects a route. Use current.json, mission artifacts, and pipeline-plan.json as bounded execution state; load only the selected route skill. ${coreEngineeringDirectiveReferenceText()} ${engineeringSanityPolicyText()} Codex native /goal is the only persisted goal owner. Use official Codex subagents only when the route requires independent slices, keep parent-owned integration, use bounded TriWiki context, fetch current external docs only when relevant, record real blockers, and finish with a completion summary that states what was verified and what remains. Reflection and Honest Mode sections are strict-profile requirements only.\n`,
|
|
71
71
|
'context7-docs': `---\nname: context7-docs\ndescription: Enforce Context7 MCP documentation evidence for SKS routes that depend on external libraries, frameworks, APIs, MCPs, package managers, DB SDKs, or generated docs.\n---\n\nWhen required, resolve-library-id, then query-docs for the resolved id. Legacy get-library-docs evidence is accepted. Prefer sks context7 tools/resolve/docs/evidence and finish only after both evidence stages exist. Check setup with sks context7 check.\n`,
|
|
72
72
|
'super-search': `---\nname: super-search\ndescription: Dollar-command route for $Super-Search provider-independent source intelligence.\n---\n\nUse when the user invokes $Super-Search or asks for Super-Search source intelligence, source acquisition, X-search-style collection, URL acquisition, source normalization, claim ledgers, or citation proof. Prefer \`sks super-search doctor --json\` for readiness and \`sks super-search run "<query>" --mode balanced --json\` for provider-independent source proof; use \`sks super-search x "<query>" --json\` for X-search intent and \`sks super-search fetch "<url>" --json\` for URL acquisition. Context7 is required only when the query depends on current package/API/framework/MCP/generated documentation behavior. Provider-specific credentials are optional and must not be required for route readiness. Evidence/artifacts remain under \`.sneakoscope/missions/<super-search-* or route mission>/super-search/\`: intent.json, axes.json, query-variants.json, provider-plan.json, source-ledger.json, lead-ledger.json, claim-ledger.json, attempt-ledger.json, synthesis.md, super-search-proof.json, super-search-gate.json, and super-search-result.json. Do not turn weak discovery into supported claims; finish with an Honest Mode summary of verified sources, blockers, and unverified external coverage.\n`,
|
|
73
73
|
'search-visibility-core': `---\nname: search-visibility-core\ndescription: Shared kernel for seo-geo-optimizer audit, plan, explicit apply, rollback, verification, gates, and Completion Proof.\n---\n\nPurpose: keep Search Engine Optimization and Generative Engine Optimization on one typed search-visibility kernel instead of duplicate implementations. Use when $SEO-GEO-OPTIMIZER or \`sks seo-geo-optimizer\` is selected. Workflow: doctor detects package/static/Next evidence; audit writes source-backed inventory and findings; plan compiles safe mutation operations; apply requires explicit \`--apply\`; verify separates source, build, HTTP, browser, production, and measured outcome; rollback only reverses mission-owned operations. Safety: default read-only, never overwrite unmanaged robots.txt, sitemap, llms.txt, metadata, or structured data; do not hard-code customer routes; do not invent prices, reviews, availability, rankings, traffic, or AI citation outcomes. Evidence/artifacts: search-visibility/intake.json, adapter-detection.json, site-inventory.json, route-graph.json, robots-policy.json, structured-data-ledger.json, mutation-plan.json, mutation-journal.jsonl, rollback-manifest.json, verification-report.json, route gate, and completion-proof.json. Failure/recovery: unsupported frameworks stay audit/plan-only; missing production/browser/Search Console evidence remains unverified, not fabricated. CLI entrypoint: \`sks seo-geo-optimizer ... --mode seo|geo\`.\n`,
|
|
74
74
|
'seo-geo-optimizer': `---\nname: seo-geo-optimizer\ndescription: Unified $SEO-GEO-OPTIMIZER route for Search Engine Optimization and Generative Engine Optimization.\n---\n\nPurpose: use one route name for SEO and GEO work while keeping the internal search-visibility mode explicit. Use when: the user asks for SEO audit/fix/verification, package/npm/GitHub search visibility, canonical, sitemap, robots.txt, hreflang, metadata, structured data, AI answer visibility, LLM citation readiness, answerability, entity/claim provenance, crawler policy, OAI-SearchBot/GPTBot/ChatGPT-User, Claude-SearchBot/ClaudeBot/Claude-User, or optional llms.txt planning. GEO means Generative Engine Optimization, not geolocation, GeoIP, maps, CDN geography, location permission, or regional redirect bugs. Workflow: run \`sks seo-geo-optimizer doctor --mode seo|geo\`, then audit, plan, explicit apply, verify, status, and rollback. Use \`--mode seo\` for technical/package search optimization and \`--mode geo\` for entity facts, claim evidence, answerability, crawler policy, and optional llms.txt. Safety: audit and plan must not mutate source; apply checks base hashes, ownership, scope, protected paths, rollback manifest, and post-verify. AI crawler policy must split search, training, user-directed retrieval, and ads/other; never use one allow_ai toggle and never auto-allow training crawlers. Evidence/artifacts: site-inventory.json, route-graph.json, seo-findings.json or geo-findings.json, entity-facts.json, claim-evidence-ledger.json, answerability-report.json, ai-crawler-policy.json, llms-txt-plan.json, mutation-plan.json, verification-report.json, seo-gate.json or geo-gate.json, completion-proof.json. Failure/recovery: unsupported frameworks stay plan-only; browser/production/Search Console/analytics outcomes are marked unverified when not actually run. Forbidden claims: no ranking, indexing, traffic lift, rich-result, answer inclusion, or AI citation guarantee; no keyword stuffing, doorway pages, fake reviews, fake prices, fake availability, fake shipping, fake awards, hidden AI-only text, or scaled spam. CLI entrypoint: \`sks seo-geo-optimizer doctor|audit|plan|apply|verify|status|rollback|fixture --mode seo|geo\`.\n`,
|
|
75
75
|
'reflection': `---\nname: reflection\ndescription: Post-route self-review for full SKS routes that records real misses, gaps, and corrective lessons into TriWiki memory.\n---\n\nUse after full route work/tests and before final. DFix, Answer, Help, Wiki, SKS discovery are exempt. Do not invent faults. Write reflection.md; append real lessons to ${REFLECTION_MEMORY_PATH}; refresh/pack, validate context-pack.json, pass reflection-gate.json.\n\n${reflectionInstructionText()}\n`,
|
|
76
|
-
'honest-mode': `---\nname: honest-mode\ndescription:
|
|
76
|
+
'honest-mode': `---\nname: honest-mode\ndescription: Optional explicit self-audit before a completion claim; run it when asked for $Honest-Mode, or when the strict verification profile requires it.\n---\n\nWhen invoked: restate the goal, compare the result to the evidence actually produced, list the tests/commands/inspections that ran, and state uncertainty or blockers plainly. Do not claim completion beyond evidence. In the essential profile this is a tool the user reaches for, not a gate every finish must pass; in the strict profile full routes still pass reflection-gate.json first and end with a concise SKS Honest Mode (솔직모드) section.\n`,
|
|
77
77
|
'autoresearch-loop': `---\nname: autoresearch-loop\ndescription: Iterative AutoResearch-style loop for open-ended improvement, discovery, prompt, ranking, SEO/GEO, and workflow-quality tasks.\n---\n\nUse for research, ranking, prompt/workflow improvement, benchmark gains, or repeated refinement. Loop: program, hypothesis, smallest falsifying experiment, metric, keep/discard, falsify, next step. Keep a ledger and do not claim improvement without evidence.\n`,
|
|
78
78
|
'hproof-claim-ledger': `---\nname: hproof-claim-ledger\ndescription: Extract atomic claims and classify support status.\n---\n\nEvery factual statement must become an atomic claim. Unsupported critical claims cannot be used for implementation or final answer. Database claims require DB safety evidence.\n`,
|
|
79
79
|
'hproof-evidence-bind': `---\nname: hproof-evidence-bind\ndescription: Bind claims to code, tests, decision contract, vgraph, beta, wiki, or GX render evidence.\n---\n\nEvidence priority: current code/tests, decision-contract.json, vgraph.json, beta.json, GX snapshot/render metadata, LLM Wiki coordinate index, user prompt. Database claims must respect .sneakoscope/db-safety.json. Wiki claims should carry id, hash, source path, and RGBA/trig coordinate anchors so they can be hydrated instead of treated as unsupported summaries.\n`,
|
package/dist/core/init.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { postToolEvidenceEnabled } from './verification-profile.js';
|
|
2
3
|
import fsp from 'node:fs/promises';
|
|
3
4
|
import { ensureDir, readJson, readText, writeJsonAtomic, writeTextAtomic, mergeManagedBlock, nowIso, PACKAGE_VERSION, exists } from './fsx.js';
|
|
4
5
|
import { DEFAULT_RETENTION_POLICY } from './retention.js';
|
|
@@ -144,9 +145,17 @@ const MANAGED_HOOKS = {
|
|
|
144
145
|
SubagentStop: [{ hooks: [{ type: 'command', command: null, hookName: 'subagent-stop', statusMessage: 'SKS recording subagent stop' }] }],
|
|
145
146
|
Stop: [{ hooks: [{ type: 'command', command: null, hookName: 'stop', statusMessage: 'SKS checking done gate' }] }]
|
|
146
147
|
};
|
|
147
|
-
|
|
148
|
+
const ESSENTIAL_PROFILE_OMITTED_HOOK_EVENTS = new Set(['PostToolUse']);
|
|
149
|
+
export function managedHookEventNames(root) {
|
|
150
|
+
const all = Object.keys(MANAGED_HOOKS);
|
|
151
|
+
return postToolEvidenceEnabled(root) ? all : all.filter((event) => !ESSENTIAL_PROFILE_OMITTED_HOOK_EVENTS.has(event));
|
|
152
|
+
}
|
|
153
|
+
function buildManagedHooks(commandPrefix, root) {
|
|
148
154
|
const hooks = {};
|
|
155
|
+
const events = new Set(managedHookEventNames(root));
|
|
149
156
|
for (const [eventName, entries] of Object.entries(MANAGED_HOOKS)) {
|
|
157
|
+
if (!events.has(eventName))
|
|
158
|
+
continue;
|
|
150
159
|
hooks[eventName] = entries.map((entry) => ({
|
|
151
160
|
...('matcher' in entry ? { matcher: entry.matcher } : {}),
|
|
152
161
|
hooks: entry.hooks.map(({ hookName, ...hook }) => ({
|
|
@@ -160,7 +169,7 @@ function buildManagedHooks(commandPrefix) {
|
|
|
160
169
|
const CODEX_HOOK_EVENT_KEYS = { ...CODEX_HOOK_EVENT_STATE_KEYS };
|
|
161
170
|
export function buildManagedHookTrustStateToml(root, commandPrefix) {
|
|
162
171
|
const source = path.join(root, '.codex', 'hooks.json');
|
|
163
|
-
const managed = buildManagedHooks(commandPrefix).hooks;
|
|
172
|
+
const managed = buildManagedHooks(commandPrefix, root).hooks;
|
|
164
173
|
const blocks = [];
|
|
165
174
|
for (const [eventName, entries] of Object.entries(managed)) {
|
|
166
175
|
const eventKey = CODEX_HOOK_EVENT_KEYS[eventName] || eventName;
|
|
@@ -217,7 +226,7 @@ function upsertCodexTrustTomlTable(text, table, block) {
|
|
|
217
226
|
lines.splice(start, end - start, ...blockLines);
|
|
218
227
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
219
228
|
}
|
|
220
|
-
export function mergeManagedHooksJson(existingContent, commandPrefix) {
|
|
229
|
+
export function mergeManagedHooksJson(existingContent, commandPrefix, projectRootDir) {
|
|
221
230
|
let root = {};
|
|
222
231
|
try {
|
|
223
232
|
root = existingContent?.trim() ? JSON.parse(existingContent) : {};
|
|
@@ -227,10 +236,10 @@ export function mergeManagedHooksJson(existingContent, commandPrefix) {
|
|
|
227
236
|
catch {
|
|
228
237
|
root = {};
|
|
229
238
|
}
|
|
230
|
-
const managed = buildManagedHooks(commandPrefix);
|
|
239
|
+
const managed = buildManagedHooks(commandPrefix, projectRootDir);
|
|
231
240
|
const currentHooks = root.hooks && typeof root.hooks === 'object' && !Array.isArray(root.hooks) ? root.hooks : {};
|
|
232
241
|
const nextHooks = { ...currentHooks };
|
|
233
|
-
for (const
|
|
242
|
+
for (const eventName of new Set([...Object.keys(currentHooks), ...Object.keys(managed.hooks)])) {
|
|
234
243
|
const existingEntries = Array.isArray(currentHooks[eventName]) ? currentHooks[eventName] : [];
|
|
235
244
|
const preserved = [];
|
|
236
245
|
for (const entry of existingEntries) {
|
|
@@ -238,7 +247,12 @@ export function mergeManagedHooksJson(existingContent, commandPrefix) {
|
|
|
238
247
|
if (stripped)
|
|
239
248
|
preserved.push(stripped);
|
|
240
249
|
}
|
|
241
|
-
|
|
250
|
+
const managedEntries = Array.isArray(managed.hooks[eventName]) ? managed.hooks[eventName] : [];
|
|
251
|
+
const merged = [...preserved, ...managedEntries];
|
|
252
|
+
if (merged.length)
|
|
253
|
+
nextHooks[eventName] = merged;
|
|
254
|
+
else if (eventName in nextHooks)
|
|
255
|
+
delete nextHooks[eventName];
|
|
242
256
|
}
|
|
243
257
|
return `${JSON.stringify({ ...root, hooks: nextHooks }, null, 2)}\n`;
|
|
244
258
|
}
|
|
@@ -282,8 +296,8 @@ const AGENTS_BLOCK = [
|
|
|
282
296
|
'',
|
|
283
297
|
'- Prefer current code, tests, configuration, contracts, and specifications over memory. Use Context7 or official vendor docs when external APIs, SDKs, packages, MCPs, or versions matter.',
|
|
284
298
|
'- Keep TriWiki recall bounded: read the current context pack before a stage, hydrate risky or stale claims from source, refresh after material changes, and validate before handoff or final.',
|
|
285
|
-
'- Mock, fixture, synthetic, or capability evidence never proves real execution.
|
|
286
|
-
'- Final output
|
|
299
|
+
'- Mock, fixture, synthetic, or capability evidence never proves real execution. Say what was actually run.',
|
|
300
|
+
'- Final output states the result, what was verified, and what remains — plainly, once. No ritual sections.',
|
|
287
301
|
'',
|
|
288
302
|
'## Safety',
|
|
289
303
|
'',
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const VERIFICATION_PROFILES = ['essential', 'strict'];
|
|
5
|
+
export const DEFAULT_VERIFICATION_PROFILE = 'essential';
|
|
6
|
+
export const VERIFICATION_PROFILE_ENV = 'SKS_VERIFICATION_PROFILE';
|
|
7
|
+
export const VERIFICATION_PROFILE_FILE = 'verification-profile.json';
|
|
8
|
+
const CACHE_TTL_MS = 5_000;
|
|
9
|
+
const cache = new Map();
|
|
10
|
+
export function normalizeVerificationProfile(value) {
|
|
11
|
+
const text = String(value ?? '').trim().toLowerCase();
|
|
12
|
+
return text === 'essential' || text === 'strict' ? text : null;
|
|
13
|
+
}
|
|
14
|
+
function globalRootDir(env) {
|
|
15
|
+
return env.SKS_GLOBAL_ROOT || path.join(env.HOME || os.homedir(), '.sneakoscope-global');
|
|
16
|
+
}
|
|
17
|
+
function readProfileFile(file) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
20
|
+
return normalizeVerificationProfile(parsed?.profile);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function isVerificationTestHarness(env = process.env) {
|
|
27
|
+
return env.NODE_TEST_CONTEXT !== undefined || env.SKS_TEST_ISOLATION === '1';
|
|
28
|
+
}
|
|
29
|
+
export function resolveVerificationProfile(root, env = process.env) {
|
|
30
|
+
const explicit = normalizeVerificationProfile(env[VERIFICATION_PROFILE_ENV]);
|
|
31
|
+
if (explicit)
|
|
32
|
+
return explicit;
|
|
33
|
+
const key = `${root ? path.resolve(root) : ''}|${globalRootDir(env)}|${isVerificationTestHarness(env) ? 't' : 'p'}`;
|
|
34
|
+
const cached = cache.get(key);
|
|
35
|
+
if (cached && Date.now() - cached.at < CACHE_TTL_MS)
|
|
36
|
+
return cached.profile;
|
|
37
|
+
const profile = (root ? readProfileFile(path.join(path.resolve(root), '.sneakoscope', VERIFICATION_PROFILE_FILE)) : null)
|
|
38
|
+
?? readProfileFile(path.join(globalRootDir(env), VERIFICATION_PROFILE_FILE))
|
|
39
|
+
?? (isVerificationTestHarness(env) ? 'strict' : DEFAULT_VERIFICATION_PROFILE);
|
|
40
|
+
cache.set(key, { at: Date.now(), profile });
|
|
41
|
+
return profile;
|
|
42
|
+
}
|
|
43
|
+
export function resetVerificationProfileCache() {
|
|
44
|
+
cache.clear();
|
|
45
|
+
}
|
|
46
|
+
export function isStrictVerification(root, env = process.env) {
|
|
47
|
+
return resolveVerificationProfile(root, env) === 'strict';
|
|
48
|
+
}
|
|
49
|
+
export function stopFinalizationRitualsEnforced(root, env = process.env) {
|
|
50
|
+
return isStrictVerification(root, env);
|
|
51
|
+
}
|
|
52
|
+
export function managedSkillDigestBlocksEnforced(root, env = process.env) {
|
|
53
|
+
return isStrictVerification(root, env);
|
|
54
|
+
}
|
|
55
|
+
export function postToolEvidenceEnabled(root, env = process.env) {
|
|
56
|
+
return isStrictVerification(root, env);
|
|
57
|
+
}
|
|
58
|
+
export function manualProofRoutesBlockReadiness(root, env = process.env) {
|
|
59
|
+
return isStrictVerification(root, env);
|
|
60
|
+
}
|
|
61
|
+
export function hookDaemonEnabled(env = process.env) {
|
|
62
|
+
const explicit = String(env.SKS_HOOK_DAEMON ?? '').trim();
|
|
63
|
+
if (explicit === '1')
|
|
64
|
+
return true;
|
|
65
|
+
if (explicit === '0')
|
|
66
|
+
return false;
|
|
67
|
+
return !isVerificationTestHarness(env);
|
|
68
|
+
}
|
|
69
|
+
export function verificationProfileSummary(root, env = process.env) {
|
|
70
|
+
const explicit = normalizeVerificationProfile(env[VERIFICATION_PROFILE_ENV]);
|
|
71
|
+
const projectFile = root ? readProfileFile(path.join(path.resolve(root), '.sneakoscope', VERIFICATION_PROFILE_FILE)) : null;
|
|
72
|
+
const globalFile = readProfileFile(path.join(globalRootDir(env), VERIFICATION_PROFILE_FILE));
|
|
73
|
+
const source = explicit ? 'env'
|
|
74
|
+
: projectFile ? 'project_file'
|
|
75
|
+
: globalFile ? 'global_file'
|
|
76
|
+
: isVerificationTestHarness(env) ? 'harness_default' : 'default';
|
|
77
|
+
const profile = explicit ?? projectFile ?? globalFile ?? (isVerificationTestHarness(env) ? 'strict' : DEFAULT_VERIFICATION_PROFILE);
|
|
78
|
+
const strict = profile === 'strict';
|
|
79
|
+
return {
|
|
80
|
+
schema: 'sks.verification-profile.v1',
|
|
81
|
+
profile,
|
|
82
|
+
source,
|
|
83
|
+
hook_daemon: hookDaemonEnabled(env),
|
|
84
|
+
post_tool_evidence: strict,
|
|
85
|
+
stop_finalization_rituals: strict,
|
|
86
|
+
managed_skill_digest_blocks: strict,
|
|
87
|
+
manual_proof_routes_block_readiness: strict,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function writeVerificationProfile(profile, target, env = process.env) {
|
|
91
|
+
const dir = target.scope === 'project'
|
|
92
|
+
? path.join(path.resolve(target.root || process.cwd()), '.sneakoscope')
|
|
93
|
+
: globalRootDir(env);
|
|
94
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
95
|
+
const file = path.join(dir, VERIFICATION_PROFILE_FILE);
|
|
96
|
+
fs.writeFileSync(file, `${JSON.stringify({ schema: 'sks.verification-profile.v1', profile }, null, 2)}\n`);
|
|
97
|
+
resetVerificationProfileCache();
|
|
98
|
+
return file;
|
|
99
|
+
}
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '
|
|
1
|
+
export const PACKAGE_VERSION = '10.0.0';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "10.0.0",
|
|
5
5
|
"description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|