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 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 9.2.6** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
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 decide. |
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
  ]
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "9.2.6"
262
+ version = "10.0.0"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "9.2.6"
3
+ version = "10.0.0"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -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() {
@@ -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] && process.env.SKS_HOOK_DAEMON === '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
  }
@@ -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: (status.readiness.warnings || []).map(String).filter(Boolean),
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": "9.2.6",
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": "3056ac543fa074868c7023097fdcb042d7175ab557d8e18cb7d92ce872785452",
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": "57e4c1d04ae785b0a53711e53210f2ddabd6131041b97edb4b6d99f18b24155e",
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": "d1216afadecd4d9c85a2fb54bd31ee77ba45f8071f43bb0e21305c5fcd870095",
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": "020ba4da2f96bcda23cee4596b1e790816780178f1917dee748ff4b289cba0e5",
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 hooksToml = CODEX_HOOK_EVENTS.map((event) => buildCodexCommandHookToml({
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 validating completion proof' : null
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 const BRIDGE_UNREACHABLE_EVIDENCE_WINDOW_MS = 10 * 60_000;
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
- blockers.add('desktop_bridge_not_ready');
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 (OPTIONAL_ROUTE_SCOPES.has(String(scope)))
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: '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.'
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
- return {
254
- decision: 'block',
255
- reason: interruptedToolOutputRecoveryBlockReason({
256
- callId: detectedMissingCallId || toolOutputQuarantine?.call_id,
257
- missionId: state?.mission_id || toolOutputQuarantine?.mission_id
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
- if (skillAvailabilityBlock) {
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;
@@ -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 summary plus Honest Mode; do not create missions, subagents, or file edits.\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 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 plus Honest Mode. ${CODEX_WEB_VERIFICATION_POLICY} ${CODEX_COMPUTER_USE_ONLY_POLICY}\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 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, evidence, reflection, and Honest Mode.\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, refresh and validate evidence, run reflection for full routes, and finish with a completion summary plus Honest Mode.\n`,
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: Required final SKS verification pass before claiming a task is complete.\n---\n\nBefore final: include a completion summary explaining what was done, what changed for the user/repo, what was verified, and what remains unverified or blocked. Then restate the goal, compare result to evidence, list tests/commands/inspections, state uncertainty or blockers plainly, and do not claim completion beyond evidence. Full routes must pass reflection-gate.json first. Include concise SKS Honest Mode or 솔직모드 when required.\n`,
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
- function buildManagedHooks(commandPrefix) {
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 [eventName, managedEntries] of Object.entries(managed.hooks)) {
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
- nextHooks[eventName] = [...preserved, ...managedEntries];
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. Completion claims require relevant checks or an explicit justification for why a check is not useful.',
286
- '- Final output must summarize the result, verification, and remaining gaps, then run Honest Mode.',
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
+ }
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '9.2.6';
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": "9.2.6",
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",