sneakoscope 3.1.9 → 3.1.10

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
@@ -35,14 +35,15 @@ Set up this agent project with Sneakoscope Codex. Use [[mandarange/Sneakoscope-C
35
35
 
36
36
  ## 🚀 Current Release
37
37
 
38
- SKS **3.1.9** hardens update/setup/doctor safety around four operator-facing failure modes: immutable core skills, duplicate project skills, real native capability repair status, and Supabase/secret preservation.
38
+ SKS **3.1.10** is a release-ready hardening pass for release wiring parity, immutable core skills, duplicate skill prevention, native capability postchecks, and protected secret rollback.
39
39
 
40
- What changed in 3.1.8:
40
+ What changed in 3.1.10:
41
41
 
42
42
  - **Core SKS skills are content-addressed and immutable.** The eight built-in route skills now have a manifest and no-drift gates; setup/update/doctor may install missing managed copies or restore corrupted managed copies, but they do not overwrite user-authored collisions.
43
- - **Duplicate skills are detected and repaired safely.** Canonical skill names collapse variants such as `Loop`, `loop`, and `loop/SKILL.md`; SKS-managed duplicates can be quarantined automatically, while user-authored duplicates require explicit confirmation.
44
- - **`sks doctor --fix` reports native capability truthfully.** Image generation, image follow-up edit paths, Computer Use, Chrome/web review, app screenshots, app handoff, and image path exposure now run through a repair matrix and postcheck instead of capability assumptions.
45
- - **Supabase keys survive setup/update/doctor.** Protected secret surfaces are fingerprinted before and after guarded operations; reports store only redacted previews and hashes, never raw values.
43
+ - **Release wiring is self-checking.** `release:gate-script-parity`, `release:wiring-3110-blackbox`, and `sks:3110-all-feature-regression` prove required ids, package scripts, release gates, source scripts, and built dist targets stay aligned.
44
+ - **Duplicate skills are detected and repaired safely.** Canonical skill names collapse variants such as `Loop`, `loop`, and `loop/SKILL.md`; SKS-managed duplicates can be quarantined automatically, while user-authored duplicates require explicit confirmation and produce active-name proof.
45
+ - **`sks doctor --fix` reports native capability truthfully.** Image generation, image follow-up edit paths, Computer Use, Chrome/web review, app screenshots, app handoff, and image path exposure now run capability-specific postchecks; manual-only and fallback surfaces do not become `verified`.
46
+ - **Supabase keys survive setup/update/doctor.** Protected secret surfaces are fingerprinted before and after guarded operations; missing or changed values are restored from backup or hard-fail without writing raw values to reports.
46
47
 
47
48
  SKS 3.0.0 was the parallel-runtime stabilization release. The whole live-swarm experience — what you actually *see* while 5, 20, or 100 workers run — was rebuilt and proven end-to-end.
48
49
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "3.1.9"
79
+ version = "3.1.10"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "3.1.9"
3
+ version = "3.1.10"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
4
4
  fn main() {
5
5
  let mut args = std::env::args().skip(1);
6
6
  match args.next().as_deref() {
7
- Some("--version") => println!("sks-rs 3.1.9"),
7
+ Some("--version") => println!("sks-rs 3.1.10"),
8
8
  Some("compact-info") => {
9
9
  let mut input = String::new();
10
10
  let _ = io::stdin().read_to_string(&mut input);
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schema": "sks.dist-build-stamp.v1",
3
3
  "package_name": "sneakoscope",
4
- "package_version": "3.1.9",
5
- "source_digest": "dbfd5ddb700abfc4a008256b36e1743363164fd504bba959542ce79c25916dad",
6
- "source_file_count": 2605,
7
- "built_at_source_time": 1781537952236
4
+ "package_version": "3.1.10",
5
+ "source_digest": "57a7f5ee1f3ac797f46e3dfd59365b1e147e4637a8ddd18fde2cbee802fec0ac",
6
+ "source_file_count": 2609,
7
+ "built_at_source_time": 1781550268084
8
8
  }
package/dist/bin/sks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '3.1.9';
2
+ const FAST_PACKAGE_VERSION = '3.1.10';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--agent' && args[1] === 'worker') {
@@ -29,8 +29,15 @@ import { buildCodexNativeFeatureMatrix } from '../core/codex-native/codex-native
29
29
  import { repairCodexNativeManagedAssets } from '../core/codex-native/codex-native-repair-transaction.js';
30
30
  import { runDoctorNativeCapabilityRepair } from '../core/doctor/doctor-native-capability-repair.js';
31
31
  import { runDoctorCommandAliasCleanup } from '../core/doctor/command-alias-cleanup.js';
32
+ import { withSecretPreservationGuard } from '../core/config/config-migration-journal.js';
32
33
  export async function run(_command, args = []) {
34
+ const root = await projectRoot();
33
35
  const doctorFix = flag(args, '--fix');
36
+ if (doctorFix)
37
+ return withSecretPreservationGuard(root, 'doctor-fix', () => runDoctor(args, root, doctorFix));
38
+ return runDoctor(args, root, doctorFix);
39
+ }
40
+ async function runDoctor(args = [], root, doctorFix) {
34
41
  let setupRepair = null;
35
42
  const sksUpdate = doctorFix
36
43
  ? {
@@ -70,7 +77,6 @@ export async function run(_command, args = []) {
70
77
  : await ensureGlobalCodexFastModeDuringInstall().catch((err) => ({ status: 'failed', error: err?.message || String(err) }))
71
78
  };
72
79
  }
73
- const root = await projectRoot();
74
80
  const commandAliasCleanup = await runDoctorCommandAliasCleanup({
75
81
  root,
76
82
  fix: doctorFix
@@ -397,6 +403,12 @@ export async function run(_command, args = []) {
397
403
  console.log(` app screenshot: ${nativeCapabilityStatus(nativeCapabilityRows, 'codex_app_screenshot', 'degraded')}`);
398
404
  console.log(` app handoff: ${nativeCapabilityStatus(nativeCapabilityRows, 'app_handoff', 'unavailable')}`);
399
405
  console.log(` image path exposure: ${nativeCapabilityStatus(nativeCapabilityRows, 'image_path_exposure', 'fallback')}`);
406
+ const nativeManualActions = uniqueNativeManualActions(nativeCapabilityRows);
407
+ if (nativeManualActions.length) {
408
+ console.log(' manual next actions:');
409
+ for (const action of nativeManualActions)
410
+ console.log(` - ${action}`);
411
+ }
400
412
  console.log('SKS Skills:');
401
413
  console.log(` core skills: ${doctorSkillStatus(doctorNativeCapabilityRepair?.core_skills)}`);
402
414
  console.log(` duplicate project skills: ${doctorDedupeStatus(doctorNativeCapabilityRepair?.skill_dedupe)}`);
@@ -408,7 +420,7 @@ export async function run(_command, args = []) {
408
420
  console.log(` report: ${commandAliasCleanup.report_path}`);
409
421
  console.log('Secret preservation:');
410
422
  console.log(` Supabase keys: ${doctorNativeCapabilityRepair?.ok === false && String((doctorNativeCapabilityRepair?.blockers || []).join(' ')).includes('secret_preservation_failed') ? 'blocked' : 'preserved'}`);
411
- console.log(' secret values: redacted');
423
+ console.log(' raw secret values: never recorded');
412
424
  console.log(` migration journal: ${doctorNativeCapabilityRepair?.secret_preservation_guard || '.sneakoscope/reports/secret-preservation-guard.json'}`);
413
425
  console.log('Codex App Harness:');
414
426
  console.log(` plugins: ${codexAppHarnessMatrix.app_features?.plugin_json ? 'ok' : 'degraded'}`);
@@ -540,6 +552,13 @@ function nativeCapabilityStatus(rows, id, fallback) {
540
552
  return fallback;
541
553
  if (row.after === 'verified' || row.before === 'verified')
542
554
  return 'verified';
555
+ if (id === 'image_path_exposure') {
556
+ if (row.before === 'degraded' || row.after === 'degraded' || row.repairability === 'doctor-fix')
557
+ return 'fallback';
558
+ return fallback;
559
+ }
560
+ if (id === 'app_handoff')
561
+ return 'unavailable';
543
562
  if (row.repairability === 'manual-required')
544
563
  return 'manual_required';
545
564
  if (row.before === 'degraded' || row.after === 'degraded')
@@ -550,6 +569,12 @@ function nativeCapabilityStatus(rows, id, fallback) {
550
569
  return 'unavailable';
551
570
  return fallback;
552
571
  }
572
+ function uniqueNativeManualActions(rows) {
573
+ return [...new Set(rows
574
+ .filter((row) => row?.repairability === 'manual-required' && row?.after !== 'verified')
575
+ .flatMap((row) => Array.isArray(row.repair_actions) ? row.repair_actions : [])
576
+ .filter((action) => typeof action === 'string' && action.trim()))];
577
+ }
553
578
  function doctorSkillStatus(coreSkills) {
554
579
  if (!coreSkills)
555
580
  return 'drift_detected';
@@ -1,11 +1,12 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
- import { ensureDir, nowIso, writeJsonAtomic } from '../fsx.js';
4
+ import { ensureDir, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
5
5
  import { buildSksCoreSkillManifest } from '../codex-native/core-skill-manifest.js';
6
6
  import { syncCoreSkillsIntegrity } from '../codex-native/core-skill-integrity.js';
7
7
  import { dedupeProjectSkills } from '../codex-native/project-skill-dedupe.js';
8
8
  const EXTERNAL_ROUTE_RESERVED = new Set(['ulw-loop', 'ulw-plan', 'start-work']);
9
+ const SKILL_SYNC_LOCK_STALE_AFTER_MS = 30000;
9
10
  export async function syncCodexSksSkills(input) {
10
11
  const root = path.resolve(input.root);
11
12
  const skillsRoot = input.skillsRoot || path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'skills');
@@ -71,6 +72,8 @@ export async function withSkillSyncLock(root, fn) {
71
72
  const code = err && typeof err === 'object' && 'code' in err ? String(err.code) : '';
72
73
  if (code !== 'EEXIST')
73
74
  throw err;
75
+ if (await recoverStaleSkillSyncLock(lockPath))
76
+ continue;
74
77
  if (Date.now() - started > 30000)
75
78
  throw new Error(`Timed out waiting for skill sync lock: ${lockPath}`);
76
79
  await new Promise((resolve) => setTimeout(resolve, 25));
@@ -80,7 +83,8 @@ export async function withSkillSyncLock(root, fn) {
80
83
  await writeJsonAtomic(path.join(lockPath, 'owner.json'), {
81
84
  schema: 'sks.skill-sync-lock.v1',
82
85
  pid: process.pid,
83
- acquired_at: nowIso()
86
+ acquired_at: nowIso(),
87
+ stale_after_ms: SKILL_SYNC_LOCK_STALE_AFTER_MS
84
88
  }).catch(() => undefined);
85
89
  return await fn();
86
90
  }
@@ -88,6 +92,37 @@ export async function withSkillSyncLock(root, fn) {
88
92
  await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
89
93
  }
90
94
  }
95
+ async function recoverStaleSkillSyncLock(lockPath) {
96
+ const ownerPath = path.join(lockPath, 'owner.json');
97
+ const stat = await fs.stat(lockPath).catch(() => null);
98
+ const owner = await readJson(ownerPath, null).catch(() => null);
99
+ const staleAfterMs = Number(owner?.stale_after_ms || SKILL_SYNC_LOCK_STALE_AFTER_MS);
100
+ const acquiredAt = owner?.acquired_at ? Date.parse(owner.acquired_at) : NaN;
101
+ const ageMs = Number.isFinite(acquiredAt) ? Date.now() - acquiredAt : stat ? Date.now() - stat.mtimeMs : 0;
102
+ if (owner?.schema === 'sks.skill-sync-lock.v1' && Number.isFinite(owner.pid)) {
103
+ if (ageMs <= staleAfterMs || pidAlive(Number(owner.pid)))
104
+ return false;
105
+ await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
106
+ return true;
107
+ }
108
+ if (stat && Date.now() - stat.mtimeMs > staleAfterMs) {
109
+ await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
110
+ return true;
111
+ }
112
+ return false;
113
+ }
114
+ function pidAlive(pid) {
115
+ if (!Number.isFinite(pid) || pid <= 0)
116
+ return false;
117
+ try {
118
+ process.kill(pid, 0);
119
+ return true;
120
+ }
121
+ catch (err) {
122
+ const code = err && typeof err === 'object' && 'code' in err ? String(err.code) : '';
123
+ return code === 'EPERM';
124
+ }
125
+ }
91
126
  async function listSkillNames(root) {
92
127
  const rows = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
93
128
  return rows.filter((row) => row.isDirectory()).map((row) => row.name).sort();
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { ensureDir, nowIso, readText, sha256, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
4
- import { buildSksCoreSkillManifest, isSksManagedCoreSkillContent, renderCoreSkillTemplate } from './core-skill-manifest.js';
4
+ import { CORE_SKILL_TEMPLATE_VERSION, buildSksCoreSkillManifest, isSksManagedCoreSkillContent, renderCoreSkillTemplate } from './core-skill-manifest.js';
5
5
  import { canonicalSkillName } from './skill-name-canonicalizer.js';
6
6
  export async function syncCoreSkillsIntegrity(input) {
7
7
  const root = path.resolve(input.root);
@@ -69,7 +69,12 @@ export async function syncCoreSkillsIntegrity(input) {
69
69
  root,
70
70
  apply,
71
71
  skills_root: skillsRoot,
72
+ template_version: CORE_SKILL_TEMPLATE_VERSION,
72
73
  manifest_sha256: sha256(JSON.stringify(manifest.skills.map((skill) => [skill.canonical_name, skill.content_sha256]))),
74
+ drift_detected_count: rows.filter((row) => row.action === 'restore-corrupted-managed-copy' || row.action === 'skip-user-authored').length,
75
+ restored_count: restored.length,
76
+ installed_count: installed.length,
77
+ user_collision_count: skippedUserAuthored.length,
73
78
  rows,
74
79
  installed,
75
80
  restored,
@@ -1,6 +1,6 @@
1
1
  import { PACKAGE_VERSION, nowIso, sha256 } from '../fsx.js';
2
2
  import { canonicalSkillName } from './skill-name-canonicalizer.js';
3
- export const CORE_SKILL_TEMPLATE_VERSION = '3.1.8-core-skill-template.v1';
3
+ export const CORE_SKILL_TEMPLATE_VERSION = 'sks-core-skill-template.v1';
4
4
  export const CORE_SKILL_MANAGED_BEGIN = '<!-- BEGIN SKS IMMUTABLE CORE SKILL -->';
5
5
  export const CORE_SKILL_MANAGED_END = '<!-- END SKS IMMUTABLE CORE SKILL -->';
6
6
  const CORE_SKILL_DEFINITIONS = [
@@ -1,22 +1,13 @@
1
+ import fs from 'node:fs/promises';
1
2
  import path from 'node:path';
2
- import { writeJsonAtomic } from '../fsx.js';
3
+ import { ensureDir, readJson, writeJsonAtomic } from '../fsx.js';
3
4
  import { buildNativeCapabilityRepairMatrix } from './native-capability-repair-matrix.js';
4
5
  export async function postcheckNativeCapabilities(input) {
5
6
  const root = path.resolve(input.root);
6
- const matrix = input.matrix || await buildNativeCapabilityRepairMatrix({ root, fixture: input.fixture || false, reportPath: null });
7
- const capabilities = matrix.capabilities.map((state) => {
8
- const verifiedAfterRepair = state.repairability === 'auto' || state.repairability === 'doctor-fix';
9
- if (state.id === 'computer_use' && process.env.SKS_COMPUTER_USE_CAPABILITY !== 'verified') {
10
- return { ...state, after: 'unknown', blockers: ['computer_use_os_permission_or_capability_unknown'] };
11
- }
12
- if (state.id === 'chrome_web_review' && process.env.SKS_CHROME_EXTENSION_READY !== '1' && input.fixture !== 'all-repairable') {
13
- return { ...state, after: 'unknown', blockers: ['codex_chrome_extension_readiness_not_verified'] };
14
- }
15
- if (state.blockers.length === 0 || verifiedAfterRepair)
16
- return { ...state, after: state.repairability === 'manual-required' ? 'unknown' : 'verified', blockers: state.repairability === 'manual-required' ? state.blockers : [] };
17
- return { ...state, after: 'blocked' };
18
- });
19
- const blockers = capabilities.flatMap((state) => state.after === 'verified' ? [] : state.blockers);
7
+ const fixture = input.fixture || false;
8
+ const matrix = input.matrix || await buildNativeCapabilityRepairMatrix({ root, fixture, reportPath: null });
9
+ const capabilities = await Promise.all(matrix.capabilities.map((state) => postcheckCapability(root, state, fixture)));
10
+ const blockers = capabilities.flatMap((state) => state.after === 'verified' || state.after === 'degraded' ? [] : state.blockers);
20
11
  const checked = {
21
12
  ...matrix,
22
13
  generated_at: new Date().toISOString(),
@@ -32,4 +23,141 @@ export async function postcheckNativeCapabilities(input) {
32
23
  await writeJsonAtomic(reportPath, checked).catch(() => undefined);
33
24
  return checked;
34
25
  }
26
+ async function postcheckCapability(root, state, fixture) {
27
+ if (state.id === 'image_generation')
28
+ return postcheckImageGeneration(state, fixture);
29
+ if (state.id === 'image_followup_edit')
30
+ return postcheckImageFollowupEdit(root, state);
31
+ if (state.id === 'computer_use')
32
+ return postcheckComputerUse(state, fixture);
33
+ if (state.id === 'chrome_web_review')
34
+ return postcheckChromeWebReview(state, fixture);
35
+ if (state.id === 'codex_app_screenshot')
36
+ return postcheckAppScreenshot(root, state);
37
+ if (state.id === 'app_handoff')
38
+ return postcheckAppHandoff(state, fixture);
39
+ if (state.id === 'image_path_exposure')
40
+ return postcheckImagePathExposure(root, state, fixture);
41
+ if (state.id === 'saved_artifact_path_contract')
42
+ return postcheckSavedArtifactPathContract(root, state);
43
+ return { ...state, after: 'blocked', blockers: [...state.blockers, `unknown_capability:${state.id}`] };
44
+ }
45
+ function postcheckImageGeneration(state, fixture) {
46
+ if (fixture === 'all-repairable' || state.before === 'verified')
47
+ return verified(state);
48
+ return {
49
+ ...state,
50
+ after: 'unknown',
51
+ blockers: ['imagegen_auth_or_codex_app_builtin_missing'],
52
+ warnings: [...new Set([...state.warnings, 'image_generation_not_verified_without_real_capability'])]
53
+ };
54
+ }
55
+ async function postcheckImageFollowupEdit(root, state) {
56
+ const contract = await validateSavedArtifactPathContract(root);
57
+ if (!contract.ok)
58
+ return { ...state, after: 'blocked', blockers: contract.blockers };
59
+ const sample = path.join(contract.imageArtifacts, 'postcheck-followup-sample.txt');
60
+ if (!(await writeReadSample(sample)))
61
+ return { ...state, after: 'blocked', blockers: ['image_followup_sample_artifact_unwritable'] };
62
+ return verified(state);
63
+ }
64
+ function postcheckComputerUse(state, _fixture) {
65
+ if (process.env.SKS_COMPUTER_USE_CAPABILITY === 'verified')
66
+ return verified(state);
67
+ return {
68
+ ...state,
69
+ after: 'unknown',
70
+ blockers: ['computer_use_os_permission_or_capability_unknown'],
71
+ warnings: [...new Set([...state.warnings, 'manual_os_permission_required'])]
72
+ };
73
+ }
74
+ function postcheckChromeWebReview(state, fixture) {
75
+ if (fixture === 'all-repairable' || process.env.SKS_CHROME_EXTENSION_READY === '1')
76
+ return verified(state);
77
+ return {
78
+ ...state,
79
+ after: 'unknown',
80
+ blockers: ['codex_chrome_extension_readiness_not_verified'],
81
+ warnings: [...new Set([...state.warnings, 'manual_chrome_extension_setup_required'])]
82
+ };
83
+ }
84
+ async function postcheckAppScreenshot(root, state) {
85
+ const dir = path.join(root, '.sneakoscope', 'app-screenshots');
86
+ const registry = path.join(dir, 'screenshot-registry.json');
87
+ if (!(await writeReadSample(path.join(dir, 'postcheck-screenshot-sample.txt')))) {
88
+ return { ...state, after: 'blocked', blockers: ['app_screenshot_directory_unwritable'] };
89
+ }
90
+ await writeJsonAtomic(registry, { schema: 'sks.app-screenshot-registry.v1', generated_at: new Date().toISOString(), screenshots: [] }).catch(() => undefined);
91
+ const json = await readJson(registry, {}).catch(() => ({}));
92
+ if (json.schema !== 'sks.app-screenshot-registry.v1')
93
+ return { ...state, after: 'blocked', blockers: ['app_screenshot_registry_invalid'] };
94
+ return verified(state);
95
+ }
96
+ function postcheckAppHandoff(state, fixture) {
97
+ if (fixture === 'all-repairable' || state.before === 'verified')
98
+ return verified(state);
99
+ return {
100
+ ...state,
101
+ after: 'unknown',
102
+ blockers: ['codex_app_handoff_not_verified'],
103
+ warnings: [...new Set([...state.warnings, 'manual_app_handoff_approval_required'])]
104
+ };
105
+ }
106
+ async function postcheckImagePathExposure(root, state, fixture) {
107
+ if (fixture === 'all-repairable' || state.before === 'verified')
108
+ return verified(state);
109
+ const contract = await validateSavedArtifactPathContract(root);
110
+ if (contract.ok) {
111
+ return {
112
+ ...state,
113
+ after: 'degraded',
114
+ blockers: [],
115
+ warnings: [...new Set([...state.warnings, 'using_saved_artifact_path_contract_fallback'])]
116
+ };
117
+ }
118
+ return { ...state, after: 'blocked', blockers: ['image_path_exposure_missing_without_fallback_contract', ...contract.blockers] };
119
+ }
120
+ async function postcheckSavedArtifactPathContract(root, state) {
121
+ const contract = await validateSavedArtifactPathContract(root);
122
+ if (!contract.ok)
123
+ return { ...state, after: 'blocked', blockers: contract.blockers };
124
+ if (!(await writeReadSample(path.join(contract.imageArtifacts, 'postcheck-contract-image.txt'))))
125
+ return { ...state, after: 'blocked', blockers: ['image_artifacts_directory_unwritable'] };
126
+ if (!(await writeReadSample(path.join(contract.appScreenshots, 'postcheck-contract-screenshot.txt'))))
127
+ return { ...state, after: 'blocked', blockers: ['app_screenshots_directory_unwritable'] };
128
+ return verified(state);
129
+ }
130
+ function verified(state) {
131
+ return { ...state, after: 'verified', blockers: [] };
132
+ }
133
+ async function validateSavedArtifactPathContract(root) {
134
+ const contractPath = path.join(root, '.sneakoscope', 'reports', 'saved-artifact-path-contract.json');
135
+ const contract = await readJson(contractPath, null).catch(() => null);
136
+ const imageArtifacts = String(contract?.image_artifacts || path.join(root, '.sneakoscope', 'image-artifacts'));
137
+ const appScreenshots = String(contract?.app_screenshots || path.join(root, '.sneakoscope', 'app-screenshots'));
138
+ const blockers = [];
139
+ if (contract?.schema !== 'sks.saved-artifact-path-contract.v1')
140
+ blockers.push('saved_artifact_path_contract_schema_invalid');
141
+ for (const dir of [imageArtifacts, appScreenshots]) {
142
+ try {
143
+ await ensureDir(dir);
144
+ await fs.access(dir);
145
+ }
146
+ catch {
147
+ blockers.push(`directory_unwritable:${path.basename(dir)}`);
148
+ }
149
+ }
150
+ return { ok: blockers.length === 0, imageArtifacts, appScreenshots, blockers };
151
+ }
152
+ async function writeReadSample(file) {
153
+ try {
154
+ await ensureDir(path.dirname(file));
155
+ await fs.writeFile(file, 'sks-native-capability-postcheck\n', 'utf8');
156
+ const text = await fs.readFile(file, 'utf8');
157
+ return text === 'sks-native-capability-postcheck\n';
158
+ }
159
+ catch {
160
+ return false;
161
+ }
162
+ }
35
163
  //# sourceMappingURL=native-capability-postcheck.js.map
@@ -109,7 +109,7 @@ async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix
109
109
  warnings: envVerified ? [] : ['manual_os_permission_required']
110
110
  };
111
111
  }
112
- const chromeReady = process.env.SKS_CHROME_EXTENSION_READY === '1' || featureOk(nativeFeatureMatrix, 'plugin_json');
112
+ const chromeReady = process.env.SKS_CHROME_EXTENSION_READY === '1';
113
113
  return {
114
114
  id,
115
115
  before: chromeReady ? 'verified' : 'unknown',
@@ -17,8 +17,16 @@ export async function dedupeProjectSkills(input) {
17
17
  const userEntries = group.filter((entry) => !entry.managed_by_sks);
18
18
  const managedEntries = group.filter((entry) => entry.managed_by_sks);
19
19
  if (userEntries.length > 0 && managedEntries.length > 0) {
20
- for (const user of userEntries)
21
- actions.push(actionRow(canonical, 'kept', user, null, 'user-authored skill preserved'));
20
+ const keepUser = userEntries[0];
21
+ if (keepUser)
22
+ actions.push(actionRow(canonical, 'kept', keepUser, null, 'user-authored skill preserved'));
23
+ const shouldMoveUserDuplicates = fix && yes && input.quarantineUserDuplicates === true;
24
+ for (const duplicateUser of userEntries.slice(1)) {
25
+ const quarantine = shouldMoveUserDuplicates ? await quarantineSkill(root, canonical, duplicateUser, 'user-authored duplicate skill') : null;
26
+ actions.push(actionRow(canonical, quarantine ? 'quarantined' : 'reported', duplicateUser, quarantine, 'user-authored duplicate skill requires --quarantine-user-duplicates --yes'));
27
+ }
28
+ if (userEntries.length > 1 && !shouldMoveUserDuplicates)
29
+ unresolvedUserDuplicates.push(canonical);
22
30
  for (const managed of managedEntries) {
23
31
  const quarantine = await maybeQuarantine(root, canonical, managed, fix, 'managed collision with user-authored skill');
24
32
  actions.push(actionRow(canonical, quarantine ? 'quarantined' : 'reported', managed, quarantine, 'managed collision with user-authored skill'));
@@ -49,7 +57,11 @@ export async function dedupeProjectSkills(input) {
49
57
  }
50
58
  }
51
59
  const duplicateNames = [...new Set(actions.filter((action) => action.action !== 'kept').map((action) => action.canonical_name))].sort();
52
- const blockers = unresolvedUserDuplicates.map((name) => `user_duplicate_requires_confirmation:${name}`);
60
+ const afterLedger = await buildSkillRegistryLedger({ root, reportPath: null });
61
+ const blockers = [
62
+ ...unresolvedUserDuplicates.map((name) => `user_duplicate_requires_confirmation:${name}`),
63
+ ...afterLedger.duplicate_active_canonical_names.map((name) => `duplicate_active_skill_name:${name}`)
64
+ ];
53
65
  const report = {
54
66
  schema: 'sks.project-skill-dedupe.v1',
55
67
  generated_at: nowIso(),
@@ -58,6 +70,9 @@ export async function dedupeProjectSkills(input) {
58
70
  fix,
59
71
  yes,
60
72
  actions,
73
+ active_unique_by_canonical_name: afterLedger.active_unique_by_canonical_name,
74
+ active_entries: afterLedger.active_entries,
75
+ duplicate_active_canonical_names: afterLedger.duplicate_active_canonical_names,
61
76
  duplicate_canonical_names: duplicateNames,
62
77
  unresolved_user_duplicates: unresolvedUserDuplicates,
63
78
  blockers
@@ -56,13 +56,20 @@ export async function buildSkillRegistryLedger(input) {
56
56
  entry.status = entry.status === 'user-owned' ? 'duplicate' : 'duplicate';
57
57
  });
58
58
  }
59
- const blockers = duplicates.map((name) => `duplicate_skill_name:${name}`);
59
+ const activeEntries = entries.filter((entry) => entry.status !== 'quarantined');
60
+ const activeGrouped = groupByCanonical(activeEntries);
61
+ const duplicateActiveNames = [...activeGrouped.entries()].filter(([, group]) => group.length > 1).map(([name]) => name).sort();
62
+ const activeUnique = duplicateActiveNames.length === 0;
63
+ const blockers = duplicateActiveNames.map((name) => `duplicate_active_skill_name:${name}`);
60
64
  const ledger = {
61
65
  schema: 'sks.skill-registry-ledger.v1',
62
66
  generated_at: nowIso(),
63
- ok: blockers.length === 0,
67
+ ok: activeUnique,
64
68
  root,
65
69
  entries,
70
+ active_unique_by_canonical_name: activeUnique,
71
+ active_entries: activeEntries,
72
+ duplicate_active_canonical_names: duplicateActiveNames,
66
73
  duplicate_canonical_names: duplicates,
67
74
  blockers
68
75
  };
@@ -185,7 +185,9 @@ export async function initCommand(args = []) {
185
185
  export async function fixPathCommand(args = []) {
186
186
  const root = await projectRoot();
187
187
  const installScope = installScopeFromArgs(args);
188
- await initProject(root, { installScope, localOnly: flag(args, '--local-only'), globalCommand: 'sks', force: true });
188
+ await withSecretPreservationGuard(root, 'fix-path-command', async () => {
189
+ await initProject(root, { installScope, localOnly: flag(args, '--local-only'), globalCommand: 'sks', force: true });
190
+ });
189
191
  const result = {
190
192
  schema: 'sks.fix-path.v1',
191
193
  ok: true,
@@ -180,7 +180,7 @@ async function researchRun(args) {
180
180
  await runResearchCycle(dir, researchWorkGraph, { cycle: 0, status: mock ? 'mock_native_orchestrator_planned' : 'native_orchestrator_planned' });
181
181
  await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_RUNNING_NO_QUESTIONS', questions_allowed: false, implementation_allowed: false, research_real_run_required: !mock, research_cycle_timeout_minutes: cycleTimeoutMinutes });
182
182
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.run.started', maxCycles, mock, cycleTimeoutMinutes, real_run_required: !mock });
183
- const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: flag(args, '--autoresearch') ? '$AutoResearch' : '$Research', prompt: mission.prompt || plan.prompt || 'Research run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount: effectiveDesiredWorkItemCount, minimumWorkItems: effectiveMinimumWorkItems, maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: true, profile, writeMode: writeMode, applyPatches: false, dryRunPatches, maxWriteAgents, roster: plan.native_agent_plan, routeCommand: 'sks research run', routeBlackboxKind: 'actual_research_command', narutoWorkGraph: nativeResearchWorkGraph });
183
+ const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: flag(args, '--autoresearch') ? '$AutoResearch' : '$Research', prompt: mission.prompt || plan.prompt || 'Research run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount: effectiveDesiredWorkItemCount, minimumWorkItems: effectiveMinimumWorkItems, maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: true, profile, writeMode: writeMode, applyPatches: false, dryRunPatches, maxWriteAgents, roster: plan.native_agent_plan, routeCommand: 'sks research run', routeBlackboxKind: 'actual_research_command', narutoWorkGraph: researchWorkGraph });
184
184
  await writeJsonAtomic(path.join(dir, 'research-native-agent-run.json'), nativeAgentRun);
185
185
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.native_agents.completed', backend: nativeAgentRun.backend, ok: nativeAgentRun.ok, proof: nativeAgentRun.proof?.status });
186
186
  if (!nativeAgentRun.ok) {
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { ensureDir, nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
3
+ import { ensureDir, nowIso, sha256, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
4
4
  import { isProtectedSecretKey, PROTECTED_SECRET_KEYS } from './supabase-secret-preservation.js';
5
5
  export async function writeManagedJsonConfig(file, current, managed) {
6
6
  const next = safeMergeObject(current, managed);
@@ -23,13 +23,14 @@ export async function writeManagedEnvConfig(file, currentText, managedLines) {
23
23
  const next = additions.length ? `${String(currentText || '').replace(/\s*$/, '\n')}${additions.join('\n')}\n` : String(currentText || '');
24
24
  return writeMergedText(file, currentText, next, 'env', protectedKeysInText(currentText));
25
25
  }
26
- export function safeMergeObject(current, managed) {
26
+ export function safeMergeObject(current, managed, prefix = '') {
27
27
  const out = { ...current };
28
28
  for (const [key, value] of Object.entries(managed)) {
29
- if (isProtectedSecretKey(key) && current[key] != null)
29
+ const dotted = prefix ? `${prefix}.${key}` : key;
30
+ if (isProtectedSecretKey(dotted) && current[key] != null)
30
31
  continue;
31
32
  if (isPlainObject(value) && isPlainObject(current[key]))
32
- out[key] = safeMergeObject(current[key], value);
33
+ out[key] = safeMergeObject(current[key], value, dotted);
33
34
  else
34
35
  out[key] = value;
35
36
  }
@@ -51,11 +52,23 @@ function upsertTomlBlockPreservingSecrets(text, block) {
51
52
  break;
52
53
  }
53
54
  }
54
- const existingSecretLines = lines.slice(start + 1, end).filter((line) => {
55
- const key = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
56
- return isProtectedSecretKey(`${header}.${key}`) || isProtectedSecretKey(key);
57
- });
58
- lines.splice(start, end - start, ...blockLines, ...existingSecretLines.filter((line) => !blockLines.includes(line)));
55
+ const existingBody = lines.slice(start + 1, end);
56
+ const nextBody = [...existingBody];
57
+ for (const managedLine of blockLines.slice(1)) {
58
+ const managedKey = managedLine.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
59
+ if (!managedKey) {
60
+ if (!nextBody.includes(managedLine))
61
+ nextBody.push(managedLine);
62
+ continue;
63
+ }
64
+ const existingIndex = nextBody.findIndex((line) => (line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '') === managedKey);
65
+ const protectedLine = isProtectedSecretKey(`${header}.${managedKey}`) || isProtectedSecretKey(managedKey);
66
+ if (existingIndex === -1)
67
+ nextBody.push(managedLine);
68
+ else if (!protectedLine)
69
+ nextBody[existingIndex] = managedLine;
70
+ }
71
+ lines.splice(start, end - start, blockLines[0] || `[${header}]`, ...nextBody);
59
72
  return lines.join('\n').replace(/\n{3,}/g, '\n\n');
60
73
  }
61
74
  async function writeMergedText(file, before, after, format, preserved) {
@@ -68,6 +81,7 @@ async function writeMergedText(file, before, after, format, preserved) {
68
81
  }
69
82
  await writeTextAtomic(file, after);
70
83
  }
84
+ const preservedSecretLineHashes = protectedSecretLineHashes(before);
71
85
  return {
72
86
  schema: 'sks.managed-config-merge.v1',
73
87
  generated_at: nowIso(),
@@ -77,6 +91,8 @@ async function writeMergedText(file, before, after, format, preserved) {
77
91
  changed: before !== after,
78
92
  backup_path: backupPath,
79
93
  protected_keys_preserved: preserved,
94
+ preserved_secret_lines_sha256: preservedSecretLineHashes,
95
+ idempotent: before === after || protectedSecretLineHashes(after).every((hash) => preservedSecretLineHashes.includes(hash)),
80
96
  blockers: []
81
97
  };
82
98
  }
@@ -88,7 +104,40 @@ function protectedKeysPresent(value) {
88
104
  return found;
89
105
  }
90
106
  function protectedKeysInText(text) {
91
- return PROTECTED_SECRET_KEYS.filter((key) => new RegExp(`(^|\\n)\\s*(?:export\\s+)?${String(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`).test(text)).map(String);
107
+ const found = new Set();
108
+ for (const key of PROTECTED_SECRET_KEYS) {
109
+ if (new RegExp(`(^|\\n)\\s*(?:export\\s+)?${String(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`).test(text))
110
+ found.add(String(key));
111
+ }
112
+ let section = '';
113
+ for (const line of String(text || '').split(/\r?\n/)) {
114
+ const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
115
+ if (sectionMatch) {
116
+ section = String(sectionMatch[1] || '');
117
+ continue;
118
+ }
119
+ const key = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
120
+ if (key && section && isProtectedSecretKey(`${section}.${key}`))
121
+ found.add(`${section}.${key}`);
122
+ }
123
+ return [...found].sort();
124
+ }
125
+ function protectedSecretLineHashes(text) {
126
+ const hashes = [];
127
+ let section = '';
128
+ for (const line of String(text || '').split(/\r?\n/)) {
129
+ const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
130
+ if (sectionMatch) {
131
+ section = String(sectionMatch[1] || '');
132
+ continue;
133
+ }
134
+ const key = line.match(/^\s*(?:export\s+)?([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
135
+ if (!key)
136
+ continue;
137
+ if (isProtectedSecretKey(key) || (section && isProtectedSecretKey(`${section}.${key}`)))
138
+ hashes.push(sha256(line));
139
+ }
140
+ return hashes.sort();
92
141
  }
93
142
  function lookupPath(value, dotted) {
94
143
  let current = value;
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import { ensureDir, nowIso, readJson, readText, sha256, writeJsonAtomic } from '../fsx.js';
5
5
  import { PROTECTED_SECRET_KEYS, PROTECTED_SUPABASE_ENV_KEYS } from './supabase-secret-preservation.js';
6
+ const activeGuardRoots = new Set();
6
7
  export async function captureSecretPreservationSnapshot(input) {
7
8
  const root = path.resolve(input.root);
8
9
  const sources = secretSources(root);
@@ -31,56 +32,83 @@ export async function captureSecretPreservationSnapshot(input) {
31
32
  }
32
33
  export async function withSecretPreservationGuard(root, operationName, fn) {
33
34
  const resolvedRoot = path.resolve(root);
35
+ if (activeGuardRoots.has(resolvedRoot))
36
+ return fn();
37
+ activeGuardRoots.add(resolvedRoot);
34
38
  const reportDir = path.join(resolvedRoot, '.sneakoscope', 'reports');
35
39
  await ensureDir(reportDir);
36
40
  const beforePath = path.join(reportDir, 'secret-preservation-before.json');
37
41
  const afterPath = path.join(reportDir, 'secret-preservation-after.json');
38
42
  const guardPath = path.join(reportDir, 'secret-preservation-guard.json');
39
43
  const before = await captureSecretPreservationSnapshot({ root: resolvedRoot, artifactPath: beforePath });
44
+ const backup = await backupSecretBearingSources(resolvedRoot, operationName, before);
40
45
  let result;
46
+ let operationError = null;
41
47
  try {
42
48
  result = await fn();
43
49
  }
44
50
  catch (err) {
45
- await writeJsonAtomic(guardPath, {
46
- schema: 'sks.secret-preservation-guard.v1',
47
- generated_at: nowIso(),
48
- ok: false,
49
- operation: operationName,
50
- before_path: beforePath,
51
- after_path: null,
52
- restored_keys_count: 0,
53
- missing_after: [],
54
- raw_values_recorded: false,
55
- operation_error: err instanceof Error ? err.message : String(err)
56
- }).catch(() => undefined);
57
- throw err;
51
+ operationError = err;
58
52
  }
59
- const after = await captureSecretPreservationSnapshot({ root: resolvedRoot, artifactPath: afterPath });
60
- const missing = missingProtectedSecrets(before, after);
61
- const report = {
62
- schema: 'sks.secret-preservation-guard.v1',
63
- generated_at: nowIso(),
64
- ok: missing.length === 0,
65
- operation: operationName,
66
- before_path: beforePath,
67
- after_path: afterPath,
68
- restored_keys_count: 0,
69
- missing_after: missing,
70
- raw_values_recorded: false
71
- };
72
- await writeJsonAtomic(guardPath, report).catch(() => undefined);
73
- if (missing.length) {
74
- throw new Error(`secret_preservation_failed:${missing.map((item) => `${item.source}:${item.key}`).join(',')}`);
53
+ try {
54
+ const after = await captureSecretPreservationSnapshot({ root: resolvedRoot, artifactPath: afterPath });
55
+ const changedOrMissing = changedOrMissingProtectedSecrets(before, after);
56
+ let rollbackAttempted = false;
57
+ let rollbackOk = false;
58
+ let restoredKeysCount = 0;
59
+ if (changedOrMissing.length) {
60
+ rollbackAttempted = true;
61
+ await restoreChangedSecretSources(changedOrMissing, backup.bySource);
62
+ const restored = await captureSecretPreservationSnapshot({
63
+ root: resolvedRoot,
64
+ artifactPath: path.join(reportDir, 'secret-preservation-after-restore.json')
65
+ });
66
+ const remaining = changedOrMissingProtectedSecrets(before, restored);
67
+ rollbackOk = remaining.length === 0;
68
+ restoredKeysCount = rollbackOk ? changedOrMissing.length : 0;
69
+ if (!rollbackOk) {
70
+ const failedReport = guardReport(operationName, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, false, backup.paths);
71
+ await writeJsonAtomic(guardPath, operationError ? { ...failedReport, ok: false, operation_error: sanitizeErrorMessage(operationError) } : failedReport).catch(() => undefined);
72
+ throw new Error(`secret_preservation_rollback_failed:${changedOrMissing.map((item) => `${safeSourceForError(resolvedRoot, item.source)}:${item.key}:${item.reason}`).join(',')}`);
73
+ }
74
+ }
75
+ const report = guardReport(operationName, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, rollbackAttempted ? rollbackOk : true, backup.paths);
76
+ if (operationError) {
77
+ report.ok = false;
78
+ report.operation_error = sanitizeErrorMessage(operationError);
79
+ }
80
+ await writeJsonAtomic(guardPath, report).catch(() => undefined);
81
+ if (operationError)
82
+ throw operationError;
83
+ if (operationName === 'doctor-fix' && rollbackAttempted) {
84
+ throw new Error(`secret_preservation_restored:${changedOrMissing.map((item) => `${safeSourceForError(resolvedRoot, item.source)}:${item.key}:${item.reason}`).join(',')}`);
85
+ }
86
+ return result;
87
+ }
88
+ finally {
89
+ activeGuardRoots.delete(resolvedRoot);
75
90
  }
76
- return result;
77
91
  }
78
92
  export function missingProtectedSecrets(before, after) {
93
+ return changedOrMissingProtectedSecrets(before, after)
94
+ .filter((item) => item.reason === 'missing')
95
+ .map((item) => ({ key: item.key, source: item.source }));
96
+ }
97
+ export function changedOrMissingProtectedSecrets(before, after) {
79
98
  const afterMap = new Map(after.fingerprints.filter((fp) => fp.present).map((fp) => [`${fp.source}\0${fp.key}`, fp]));
80
99
  return before.fingerprints
81
100
  .filter((fp) => fp.present && fp.value_sha256)
82
- .filter((fp) => !afterMap.has(`${fp.source}\0${fp.key}`))
83
- .map((fp) => ({ key: fp.key, source: fp.source }));
101
+ .map((fp) => {
102
+ const afterFp = afterMap.get(`${fp.source}\0${fp.key}`);
103
+ if (!afterFp) {
104
+ return { key: fp.key, source: fp.source, before_sha256: fp.value_sha256, after_sha256: null, reason: 'missing' };
105
+ }
106
+ if (afterFp.value_sha256 !== fp.value_sha256) {
107
+ return { key: fp.key, source: fp.source, before_sha256: fp.value_sha256, after_sha256: afterFp.value_sha256, reason: 'changed' };
108
+ }
109
+ return null;
110
+ })
111
+ .filter((item) => Boolean(item));
84
112
  }
85
113
  function secretSources(root) {
86
114
  const home = process.env.HOME || os.homedir();
@@ -90,8 +118,10 @@ function secretSources(root) {
90
118
  '.env.development',
91
119
  '.env.production',
92
120
  '.sneakoscope/config.json',
93
- '.codex/config.toml'
94
- ].map((rel) => path.join(root, rel)).concat(path.join(home, '.codex', 'config.toml'));
121
+ '.codex/config.toml',
122
+ '.cursor/mcp.json',
123
+ 'mcp.json'
124
+ ].map((rel) => path.join(root, rel)).concat(path.join(home, '.codex', 'config.toml'), path.join(home, '.config', 'sks', 'config.json'));
95
125
  }
96
126
  function fingerprintsFromText(text, source) {
97
127
  const rows = [];
@@ -101,6 +131,7 @@ function fingerprintsFromText(text, source) {
101
131
  continue;
102
132
  rows.push(fingerprint(String(key), source, value));
103
133
  }
134
+ rows.push(...fingerprintsFromTomlSections(text, source));
104
135
  for (const envKey of PROTECTED_SUPABASE_ENV_KEYS) {
105
136
  const value = readAssignment(text, envKey);
106
137
  if (value)
@@ -118,6 +149,25 @@ function fingerprintsFromObject(value, source) {
118
149
  }
119
150
  return rows;
120
151
  }
152
+ function fingerprintsFromTomlSections(text, source) {
153
+ const rows = [];
154
+ let section = '';
155
+ for (const line of String(text || '').split(/\r?\n/)) {
156
+ const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
157
+ if (sectionMatch) {
158
+ section = String(sectionMatch[1] || '').trim();
159
+ continue;
160
+ }
161
+ const kv = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*(.+?)\s*$/);
162
+ if (!kv || !section)
163
+ continue;
164
+ const key = `${section}.${kv[1]}`;
165
+ if (!PROTECTED_SECRET_KEYS.includes(key))
166
+ continue;
167
+ rows.push(fingerprint(key, source, unquote(String(kv[2] || ''))));
168
+ }
169
+ return rows;
170
+ }
121
171
  function fingerprint(key, source, value) {
122
172
  return {
123
173
  key,
@@ -143,9 +193,7 @@ function redactPreview(value) {
143
193
  const text = String(value || '');
144
194
  if (!text)
145
195
  return '';
146
- const head = text.slice(0, Math.min(3, text.length));
147
- const tail = text.length > 6 ? text.slice(-3) : '';
148
- return `${head}...${tail || 'redacted'}(${text.length})`;
196
+ return `sha256:${sha256(text).slice(0, 12)}(${text.length})`;
149
197
  }
150
198
  function flattenObject(value, prefix = '') {
151
199
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -166,4 +214,64 @@ function dedupeFingerprints(fingerprints) {
166
214
  byKey.set(`${fp.source}\0${fp.key}`, fp);
167
215
  return [...byKey.values()].sort((a, b) => a.source.localeCompare(b.source) || a.key.localeCompare(b.key));
168
216
  }
217
+ function guardReport(operation, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, rollbackOk, backupPaths) {
218
+ return {
219
+ schema: 'sks.secret-preservation-guard.v1',
220
+ generated_at: nowIso(),
221
+ ok: changedOrMissing.length === 0 || rollbackOk,
222
+ operation,
223
+ before_path: beforePath,
224
+ after_path: afterPath,
225
+ restored_keys_count: restoredKeysCount,
226
+ changed_or_missing: changedOrMissing,
227
+ missing_after: changedOrMissing.filter((item) => item.reason === 'missing').map((item) => ({ key: item.key, source: item.source })),
228
+ rollback_attempted: rollbackAttempted,
229
+ rollback_ok: rollbackOk,
230
+ backup_paths: backupPaths,
231
+ raw_values_recorded: false
232
+ };
233
+ }
234
+ async function backupSecretBearingSources(root, operationName, snapshot) {
235
+ const bySource = new Map();
236
+ const sources = [...new Set(snapshot.fingerprints.filter((fp) => fp.present).map((fp) => fp.source))];
237
+ if (!sources.length)
238
+ return { bySource, paths: [] };
239
+ const backupRoot = path.join(root, '.sneakoscope', 'backups', 'secrets', sanitizeSegment(operationName), new Date().toISOString().replace(/[:.]/g, '-'));
240
+ for (const source of sources) {
241
+ const backupPath = path.join(backupRoot, sanitizeSourcePath(root, source));
242
+ await ensureDir(path.dirname(backupPath));
243
+ await fs.copyFile(source, backupPath);
244
+ bySource.set(source, backupPath);
245
+ }
246
+ return { bySource, paths: [...bySource.values()] };
247
+ }
248
+ async function restoreChangedSecretSources(changedOrMissing, backups) {
249
+ for (const source of [...new Set(changedOrMissing.map((item) => item.source))]) {
250
+ const backup = backups.get(source);
251
+ if (!backup)
252
+ continue;
253
+ await ensureDir(path.dirname(source));
254
+ await fs.copyFile(backup, source);
255
+ }
256
+ }
257
+ function sanitizeSegment(value) {
258
+ return String(value || 'operation').replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'operation';
259
+ }
260
+ function sanitizeSourcePath(root, source) {
261
+ return safeSourceForError(root, source).replace(/[^A-Za-z0-9._/-]+/g, '_').replace(/^\/+/, '');
262
+ }
263
+ function safeSourceForError(root, source) {
264
+ const rel = path.relative(root, source);
265
+ if (rel && !rel.startsWith('..') && !path.isAbsolute(rel))
266
+ return rel;
267
+ const home = process.env.HOME || os.homedir();
268
+ const homeRel = path.relative(home, source);
269
+ if (homeRel && !homeRel.startsWith('..') && !path.isAbsolute(homeRel))
270
+ return `~/${homeRel}`;
271
+ return path.basename(source);
272
+ }
273
+ function sanitizeErrorMessage(err) {
274
+ const message = err instanceof Error ? err.message : String(err);
275
+ return message.replace(/([A-Za-z0-9_]*(?:SECRET|TOKEN|KEY|PASSWORD)[A-Za-z0-9_]*=)[^\s,;]+/gi, '$1<redacted>');
276
+ }
169
277
  //# sourceMappingURL=secret-preservation.js.map
package/dist/core/fsx.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- export const PACKAGE_VERSION = '3.1.9';
8
+ export const PACKAGE_VERSION = '3.1.10';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
package/dist/core/init.js CHANGED
@@ -11,7 +11,8 @@ import { AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_
11
11
  import { SKILL_DREAM_POLICY, skillDreamPolicyText } from './skill-forge.js';
12
12
  import { CODEX_HOOK_EVENT_STATE_KEYS } from './codex-compat/codex-hook-events.js';
13
13
  import { codexCommandHookCurrentHash } from './codex-hooks/codex-hook-hash.js';
14
- import { buildSksCoreSkillManifest, isCoreSkillName, renderCoreSkillTemplate } from './codex-native/core-skill-manifest.js';
14
+ import { buildSksCoreSkillManifest, isCoreSkillName } from './codex-native/core-skill-manifest.js';
15
+ import { syncCoreSkillsIntegrity } from './codex-native/core-skill-integrity.js';
15
16
  const REFLECTION_MEMORY_PATH = '.sneakoscope/memory/q2_facts/post-route-reflection.md';
16
17
  const SKS_GENERATED_GIT_PATTERNS = [
17
18
  '.sneakoscope/missions/',
@@ -1084,23 +1085,45 @@ export async function installSkills(root) {
1084
1085
  'design-ui-editor': `---\nname: design-ui-editor\ndescription: Legacy fallback UI/UX editor for existing design.md systems when Product Design plugin is unavailable.\n---\n\nUse Product Design plugin first. When falling back, read \`design.md\`, inspect relevant UI/assets/tests, consult getdesign-reference when improving the design system, apply the smallest design-system-conformant change, use imagegen for image/logo/raster assets, and verify render quality. ${productDesignPluginPolicyText()} ${CODEX_IMAGEGEN_REQUIRED_POLICY} If design.md is missing and Product Design is unavailable, use design-system-builder as fallback.\n`,
1085
1086
  'design-artifact-expert': `---\nname: design-artifact-expert\ndescription: Legacy fallback for high-fidelity HTML/UI/prototype artifacts when Product Design plugin cannot be used.\n---\n\nUse Product Design plugin first for design/UI/prototype work. When falling back, read design.md when present, consult getdesign-reference for design-system grounding, build the usable artifact first, preserve state, verify overlap/readability/responsiveness, and use imagegen for required assets. ${productDesignPluginPolicyText()} ${CODEX_IMAGEGEN_REQUIRED_POLICY}\n`
1086
1087
  };
1087
- for (const skill of buildSksCoreSkillManifest().skills) {
1088
- skills[skill.canonical_name] = renderCoreSkillTemplate(skill.canonical_name);
1089
- }
1088
+ const nonCoreSkillNames = Object.keys(skills).filter((name) => !isCoreSkillName(name));
1090
1089
  for (const [name, content] of Object.entries(skills)) {
1090
+ if (isCoreSkillName(name))
1091
+ continue;
1091
1092
  const dir = path.join(root, '.agents', 'skills', name);
1092
- const skillContent = isCoreSkillName(name) ? content : enrichSkillContent(name, content);
1093
+ const skillContent = enrichSkillContent(name, content);
1093
1094
  await ensureDir(dir);
1094
1095
  await writeTextAtomic(path.join(dir, 'SKILL.md'), `${skillContent.trim()}\n`);
1095
1096
  await writeSkillMetadata(dir, name);
1096
1097
  }
1097
- const skillNames = Object.keys(skills);
1098
+ const coreManifest = buildSksCoreSkillManifest();
1099
+ const coreByName = new Map(coreManifest.skills.map((skill) => [skill.canonical_name, skill.content_sha256]));
1100
+ const coreIntegrity = await syncCoreSkillsIntegrity({
1101
+ root,
1102
+ apply: true,
1103
+ skillsRoot: path.join(root, '.agents', 'skills'),
1104
+ reportPath: path.join(root, '.sneakoscope', 'reports', 'core-skill-integrity.json')
1105
+ });
1106
+ const managedCoreSkillNames = coreIntegrity.rows
1107
+ .filter((row) => row.after_sha256 === coreByName.get(row.canonical_name) && row.action !== 'skip-user-authored')
1108
+ .map((row) => row.canonical_name);
1109
+ for (const name of managedCoreSkillNames) {
1110
+ await writeSkillMetadata(path.join(root, '.agents', 'skills', name), name);
1111
+ }
1112
+ const skillNames = [...nonCoreSkillNames, ...managedCoreSkillNames];
1098
1113
  const removedStaleGeneratedSkills = await removeStaleGeneratedSkillsFromManifest(root, skillNames);
1099
1114
  const removedPluginSkillCollisions = await removeGeneratedPluginSkillCollisions(root);
1100
1115
  await writeGeneratedSkillManifest(root, skillNames);
1101
1116
  return {
1102
1117
  installed_skills: skillNames,
1103
1118
  generated_files: generatedSkillFiles(skillNames),
1119
+ core_skill_integrity: {
1120
+ ok: coreIntegrity.ok,
1121
+ template_version: coreIntegrity.template_version,
1122
+ installed_count: coreIntegrity.installed_count,
1123
+ restored_count: coreIntegrity.restored_count,
1124
+ user_collision_count: coreIntegrity.user_collision_count,
1125
+ report: '.sneakoscope/reports/core-skill-integrity.json'
1126
+ },
1104
1127
  removed_stale_generated_skills: [...removedStaleGeneratedSkills, ...removedPluginSkillCollisions].sort(),
1105
1128
  removed_agent_skill_aliases: await removeGeneratedAgentSkillAliases(root, skillNames),
1106
1129
  removed_codex_skill_mirrors: await removeGeneratedCodexSkillMirrors(root, skillNames)
@@ -1137,6 +1160,8 @@ async function removeStaleGeneratedSkillsFromManifest(root, skillNames) {
1137
1160
  const skillName = String(name || '').trim();
1138
1161
  if (!skillName || current.has(skillName) || !/^[a-z0-9-]+$/.test(skillName))
1139
1162
  continue;
1163
+ if (isCoreSkillName(skillName))
1164
+ continue;
1140
1165
  const dir = path.join(root, '.agents', 'skills', skillName);
1141
1166
  if (!(await exists(dir)))
1142
1167
  continue;
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '3.1.9';
1
+ export const PACKAGE_VERSION = '3.1.10';
2
2
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "3.1.9",
4
+ "version": "3.1.10",
5
5
  "description": "Sneakoscope Codex: fast proof-first Codex trust layer with image-based Voxel TriWiki.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -643,6 +643,10 @@
643
643
  "update:preserves-supabase-keys": "node ./dist/scripts/update-preserves-supabase-keys-blackbox.js",
644
644
  "update:secret-preservation-guard": "node ./dist/scripts/update-secret-preservation-guard-check.js",
645
645
  "update:secret-migration-journal": "node ./dist/scripts/update-secret-migration-journal-check.js",
646
+ "config:managed-merge-callsite-coverage": "node ./dist/scripts/config-managed-merge-callsite-coverage-check.js",
647
+ "release:gate-script-parity": "node ./dist/scripts/release-gate-script-parity-check.js",
648
+ "release:wiring-3110-blackbox": "node ./dist/scripts/release-wiring-3110-blackbox.js",
649
+ "sks:3110-all-feature-regression": "node ./dist/scripts/sks-3110-all-feature-regression-blackbox.js",
646
650
  "release:dag-full-coverage": "node ./dist/scripts/release-dag-full-coverage-check.js",
647
651
  "release:cache-glob-hashing": "node ./dist/scripts/release-cache-glob-hashing-check.js",
648
652
  "release:check:affected": "npm run build --silent && node ./dist/scripts/release-gate-dag-runner.js --preset affected --changed-since auto",