sneakoscope 3.1.9 → 3.1.11

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.11** is a release-ready repair pass for MAD Zellij stacked panes, Context7 MCP doctor recovery, and stale Codex startup config.
39
39
 
40
- What changed in 3.1.8:
40
+ What changed in 3.1.11:
41
41
 
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.
42
+ - **MAD Zellij panes require native stacked-pane support.** `sks doctor --fix` now treats Zellij 0.43.0 as the minimum interactive runtime so `sks --mad` can use native stacked worker panes instead of fragmenting into plain splits.
43
+ - **Context7 stdio lockups are doctor-repairable.** `sks doctor --fix` detects local `@upstash/context7-mcp` stdio config and migrates it to the remote Context7 MCP endpoint so Codex launches do not stall at the stdio server banner.
44
+ - **Codex startup warnings are doctor-repairable.** `sks doctor --fix` rewrites stale SKS agent `config_file` paths to existing absolute files, removes unsupported managed `message_role_prefix` role fields, preserves optional `supabase_sauron`, and drops missing-command `node_repl` MCP blocks that would otherwise spam startup.
45
+ - **Doctor JSON exposes the Context7 and startup repair reports.** `context7_repair`, `codex_startup_repair`, and their `repair.*` entries carry migration status, backups, actions, warnings, and any manual auth actions.
46
+ - **Release metadata is aligned for 3.1.11.** Package, lockfile, CLI version constants, Rust helper metadata, README, and changelog all point at the same release.
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.11"
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.11"
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.11"),
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.11",
5
+ "source_digest": "932de0e70bb2a5b787aef2782fa56cbc4956c6077d099d9e1067b9406fd8412a",
6
+ "source_file_count": 2613,
7
+ "built_at_source_time": 1781554790419
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.11';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--agent' && args[1] === 'worker') {
@@ -24,13 +24,22 @@ import { runCodex0138Doctor } from '../core/doctor/codex-0138-doctor.js';
24
24
  import { writeCodexPluginInventoryArtifacts, pluginAppTemplatePolicy } from '../core/codex-plugins/codex-plugin-json.js';
25
25
  import { writeMcpPluginInventoryArtifacts } from '../core/mcp/mcp-plugin-inventory.js';
26
26
  import { runDoctorZellijRepair, doctorZellijRepairConsoleLine } from '../core/doctor/doctor-zellij-repair.js';
27
+ import { runDoctorContext7Repair } from '../core/doctor/doctor-context7-repair.js';
28
+ import { runDoctorCodexStartupRepair } from '../core/doctor/doctor-codex-startup-repair.js';
27
29
  import { buildCodexAppHarnessMatrix } from '../core/codex-app/codex-app-harness-matrix.js';
28
30
  import { buildCodexNativeFeatureMatrix } from '../core/codex-native/codex-native-feature-broker.js';
29
31
  import { repairCodexNativeManagedAssets } from '../core/codex-native/codex-native-repair-transaction.js';
30
32
  import { runDoctorNativeCapabilityRepair } from '../core/doctor/doctor-native-capability-repair.js';
31
33
  import { runDoctorCommandAliasCleanup } from '../core/doctor/command-alias-cleanup.js';
34
+ import { withSecretPreservationGuard } from '../core/config/config-migration-journal.js';
32
35
  export async function run(_command, args = []) {
36
+ const root = await projectRoot();
33
37
  const doctorFix = flag(args, '--fix');
38
+ if (doctorFix)
39
+ return withSecretPreservationGuard(root, 'doctor-fix', () => runDoctor(args, root, doctorFix));
40
+ return runDoctor(args, root, doctorFix);
41
+ }
42
+ async function runDoctor(args = [], root, doctorFix) {
34
43
  let setupRepair = null;
35
44
  const sksUpdate = doctorFix
36
45
  ? {
@@ -70,7 +79,6 @@ export async function run(_command, args = []) {
70
79
  : await ensureGlobalCodexFastModeDuringInstall().catch((err) => ({ status: 'failed', error: err?.message || String(err) }))
71
80
  };
72
81
  }
73
- const root = await projectRoot();
74
82
  const commandAliasCleanup = await runDoctorCommandAliasCleanup({
75
83
  root,
76
84
  fix: doctorFix
@@ -112,6 +120,19 @@ export async function run(_command, args = []) {
112
120
  requireActualCodex: flag(args, '--fix') || flag(args, '--require-actual-codex'),
113
121
  codexBin: codexBin || undefined
114
122
  };
123
+ let codexStartupRepair = await runDoctorCodexStartupRepair({ root, fix: doctorFix }).catch((err) => ({
124
+ schema: 'sks.doctor-codex-startup-repair.v1',
125
+ ok: false,
126
+ generated_at: new Date().toISOString(),
127
+ fix: doctorFix,
128
+ configs: [],
129
+ agent_role_files: { sanitized: [], created: [], blockers: [err?.message || String(err)] },
130
+ actions: [],
131
+ manual_actions: [],
132
+ blockers: [err?.message || String(err)],
133
+ warnings: [],
134
+ report_path: `${root}/.sneakoscope/reports/doctor-codex-startup-repair.json`
135
+ }));
115
136
  const codexDoctorBefore = flag(args, '--fix') ? await runCodexDoctorBridge({ codexBin: codexBin || null, cwd: root, required: flag(args, '--require-actual-codex') }).catch(() => null) : null;
116
137
  const configRepair = flag(args, '--fix') ? await repairCodexConfigEperm(root, { fix: true, ...configProbeOpts }) : null;
117
138
  const migrationJournal = flag(args, '--fix')
@@ -120,6 +141,7 @@ export async function run(_command, args = []) {
120
141
  const codexConfig = configRepair?.after || await inspectCodexConfigReadability(root, configProbeOpts);
121
142
  const codexDoctor = await runCodexDoctorBridge({ codexBin: codexBin || null, cwd: root, required: flag(args, '--require-actual-codex') });
122
143
  const codexDoctorDiff = compareCodexDoctorBridge(codexDoctorBefore, codexDoctor);
144
+ codexStartupRepair = mergeObservedCodexStartupWarnings(codexStartupRepair, codexDoctor);
123
145
  const codex = codexBin
124
146
  ? { bin: codexBin, version: 'fixture-or-explicit', available: true }
125
147
  : await getCodexInfo().catch(() => ({ bin: null, version: null, available: false }));
@@ -206,6 +228,18 @@ export async function run(_command, args = []) {
206
228
  blockers: [err?.message || String(err)],
207
229
  warnings: []
208
230
  }));
231
+ const context7Repair = await runDoctorContext7Repair({ root, fix: doctorFix }).catch((err) => ({
232
+ schema: 'sks.doctor-context7-repair.v1',
233
+ ok: false,
234
+ generated_at: new Date().toISOString(),
235
+ fix: doctorFix,
236
+ preferred_transport: 'remote',
237
+ configs: [],
238
+ actions: [],
239
+ blockers: [err?.message || String(err)],
240
+ warnings: [],
241
+ report_path: `${root}/.sneakoscope/reports/doctor-context7-repair.json`
242
+ }));
209
243
  const zellij = await checkZellijCapability({ root, require: process.env.SKS_REQUIRE_ZELLIJ === '1' });
210
244
  const localModel = await readLocalModelConfig().catch(() => null);
211
245
  const permissionProfiles = await inventoryCodexPermissionProfiles(root, { writeReport: true });
@@ -285,6 +319,8 @@ export async function run(_command, args = []) {
285
319
  codex_doctor: codexDoctor,
286
320
  require_codex_doctor: flag(args, '--fix') || flag(args, '--require-actual-codex'),
287
321
  zellij,
322
+ context7_repair: context7Repair,
323
+ codex_startup_repair: codexStartupRepair,
288
324
  local_model: localModel,
289
325
  agent_role_config: agentRoleConfigRepair,
290
326
  repair: configRepair,
@@ -298,6 +334,7 @@ export async function run(_command, args = []) {
298
334
  ...(codexConfig.operator_actions || []),
299
335
  ...(configRepair?.operator_actions || []),
300
336
  ...(zellijRepair && !zellijRepair.ok && zellijRepair.command ? [`Run: ${zellijRepair.command}`] : []),
337
+ ...(codexStartupRepair.manual_actions || []),
301
338
  ...(pluginPolicy?.doctor_warnings || [])
302
339
  ]
303
340
  });
@@ -305,7 +342,7 @@ export async function run(_command, args = []) {
305
342
  const runtimeReadiness = buildRuntimeReadiness(zellijReadiness, codexNativeFeatureMatrix);
306
343
  const result = {
307
344
  schema: 'sks.doctor-status.v1',
308
- ok: ready.ready && (!sksUpdate || sksUpdate.ok !== false) && commandAliasCleanup.ok !== false,
345
+ ok: ready.ready && (!sksUpdate || sksUpdate.ok !== false) && commandAliasCleanup.ok !== false && codexStartupRepair.ok !== false,
309
346
  root,
310
347
  node: { ok: Number(process.versions.node.split('.')[0]) >= 20, version: process.version },
311
348
  codex,
@@ -319,6 +356,8 @@ export async function run(_command, args = []) {
319
356
  codex_doctor_diff: codexDoctorDiff,
320
357
  zellij,
321
358
  zellij_repair: zellijRepair,
359
+ context7_repair: context7Repair,
360
+ codex_startup_repair: codexStartupRepair,
322
361
  local_model: localModel,
323
362
  agent_role_config: agentRoleConfigRepair,
324
363
  zellij_readiness: zellijReadiness,
@@ -342,7 +381,7 @@ export async function run(_command, args = []) {
342
381
  ready,
343
382
  sneakoscope: { ok: await exists(`${root}/.sneakoscope`) },
344
383
  package: { bytes: pkgBytes, human: formatBytes(pkgBytes) },
345
- repair: { sks_update: sksUpdate, setup: setupRepair, codex_config: configRepair, migration_journal: migrationJournal, global_sks_installs: globalSksInstallCleanup, agent_role_config: agentRoleConfigRepair, zellij: zellijRepair, codex_native: codexNativeRepair, doctor_native_capability: doctorNativeCapabilityRepair, command_aliases: commandAliasCleanup }
384
+ repair: { sks_update: sksUpdate, setup: setupRepair, codex_config: configRepair, migration_journal: migrationJournal, global_sks_installs: globalSksInstallCleanup, agent_role_config: agentRoleConfigRepair, zellij: zellijRepair, context7: context7Repair, codex_startup: codexStartupRepair, codex_native: codexNativeRepair, doctor_native_capability: doctorNativeCapabilityRepair, command_aliases: commandAliasCleanup }
346
385
  };
347
386
  if (flag(args, '--json')) {
348
387
  printJson(result);
@@ -369,6 +408,21 @@ export async function run(_command, args = []) {
369
408
  const zellijRepairLine = doctorZellijRepairConsoleLine(zellijRepair);
370
409
  if (zellijRepairLine)
371
410
  console.log(zellijRepairLine);
411
+ console.log('Context7 MCP:');
412
+ console.log(` transport: ${context7Repair.preferred_transport || 'remote'}`);
413
+ console.log(` repair: ${context7Repair.ok ? 'ok' : 'blocked'}`);
414
+ for (const action of context7Repair.actions || [])
415
+ console.log(` - ${action}`);
416
+ for (const warning of context7Repair.warnings || [])
417
+ console.log(` warning: ${warning}`);
418
+ console.log('Codex startup config:');
419
+ console.log(` repair: ${codexStartupRepair.ok ? 'ok' : 'blocked'}`);
420
+ for (const action of codexStartupRepair.actions || [])
421
+ console.log(` - ${action}`);
422
+ for (const action of codexStartupRepair.manual_actions || [])
423
+ console.log(` manual: ${action}`);
424
+ for (const warning of codexStartupRepair.warnings || [])
425
+ console.log(` warning: ${warning}`);
372
426
  console.log(` codex doctor: ${codexDoctor.available ? (codexDoctor.exit_code === 0 ? 'ok' : 'warning') : 'unavailable'}`);
373
427
  console.log(`Rust acc.: ${rust.mode || (rust.available ? 'rust_accelerated' : 'js_fallback')} ${rust.version || rust.status || ''}`);
374
428
  console.log(`Codex App: ${ready.codex_app_ready ? 'ok' : 'optional_missing'}`);
@@ -397,6 +451,12 @@ export async function run(_command, args = []) {
397
451
  console.log(` app screenshot: ${nativeCapabilityStatus(nativeCapabilityRows, 'codex_app_screenshot', 'degraded')}`);
398
452
  console.log(` app handoff: ${nativeCapabilityStatus(nativeCapabilityRows, 'app_handoff', 'unavailable')}`);
399
453
  console.log(` image path exposure: ${nativeCapabilityStatus(nativeCapabilityRows, 'image_path_exposure', 'fallback')}`);
454
+ const nativeManualActions = uniqueNativeManualActions(nativeCapabilityRows);
455
+ if (nativeManualActions.length) {
456
+ console.log(' manual next actions:');
457
+ for (const action of nativeManualActions)
458
+ console.log(` - ${action}`);
459
+ }
400
460
  console.log('SKS Skills:');
401
461
  console.log(` core skills: ${doctorSkillStatus(doctorNativeCapabilityRepair?.core_skills)}`);
402
462
  console.log(` duplicate project skills: ${doctorDedupeStatus(doctorNativeCapabilityRepair?.skill_dedupe)}`);
@@ -408,7 +468,7 @@ export async function run(_command, args = []) {
408
468
  console.log(` report: ${commandAliasCleanup.report_path}`);
409
469
  console.log('Secret preservation:');
410
470
  console.log(` Supabase keys: ${doctorNativeCapabilityRepair?.ok === false && String((doctorNativeCapabilityRepair?.blockers || []).join(' ')).includes('secret_preservation_failed') ? 'blocked' : 'preserved'}`);
411
- console.log(' secret values: redacted');
471
+ console.log(' raw secret values: never recorded');
412
472
  console.log(` migration journal: ${doctorNativeCapabilityRepair?.secret_preservation_guard || '.sneakoscope/reports/secret-preservation-guard.json'}`);
413
473
  console.log('Codex App Harness:');
414
474
  console.log(` plugins: ${codexAppHarnessMatrix.app_features?.plugin_json ? 'ok' : 'degraded'}`);
@@ -540,6 +600,13 @@ function nativeCapabilityStatus(rows, id, fallback) {
540
600
  return fallback;
541
601
  if (row.after === 'verified' || row.before === 'verified')
542
602
  return 'verified';
603
+ if (id === 'image_path_exposure') {
604
+ if (row.before === 'degraded' || row.after === 'degraded' || row.repairability === 'doctor-fix')
605
+ return 'fallback';
606
+ return fallback;
607
+ }
608
+ if (id === 'app_handoff')
609
+ return 'unavailable';
543
610
  if (row.repairability === 'manual-required')
544
611
  return 'manual_required';
545
612
  if (row.before === 'degraded' || row.after === 'degraded')
@@ -550,6 +617,12 @@ function nativeCapabilityStatus(rows, id, fallback) {
550
617
  return 'unavailable';
551
618
  return fallback;
552
619
  }
620
+ function uniqueNativeManualActions(rows) {
621
+ return [...new Set(rows
622
+ .filter((row) => row?.repairability === 'manual-required' && row?.after !== 'verified')
623
+ .flatMap((row) => Array.isArray(row.repair_actions) ? row.repair_actions : [])
624
+ .filter((action) => typeof action === 'string' && action.trim()))];
625
+ }
553
626
  function doctorSkillStatus(coreSkills) {
554
627
  if (!coreSkills)
555
628
  return 'drift_detected';
@@ -696,4 +769,30 @@ function readOption(args = [], name, fallback = null) {
696
769
  const index = args.indexOf(name);
697
770
  return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
698
771
  }
772
+ function mergeObservedCodexStartupWarnings(startupRepair, codexDoctor) {
773
+ const text = `${codexDoctor?.stdout_tail || ''}\n${codexDoctor?.stderr_tail || ''}`;
774
+ const manual = new Set(Array.isArray(startupRepair?.manual_actions) ? startupRepair.manual_actions : []);
775
+ const warnings = new Set(Array.isArray(startupRepair?.warnings) ? startupRepair.warnings : []);
776
+ const blockers = new Set(Array.isArray(startupRepair?.blockers) ? startupRepair.blockers : []);
777
+ if (/codex_apps[\s\S]{0,500}token_expired|token_expired[\s\S]{0,500}codex_apps/i.test(text)) {
778
+ manual.add('Codex Apps MCP token is expired; sign in to Codex App/CLI again so the connector can mint a fresh token.');
779
+ warnings.add('codex_apps_token_expired_observed');
780
+ blockers.add('codex_apps_token_expired_manual_reauth_required');
781
+ }
782
+ if (/SUPABASE_ACCESS_TOKEN[\s\S]{0,500}mcp server ['"`]?supabase['"`]?|mcp server ['"`]?supabase['"`]?[\s\S]{0,500}SUPABASE_ACCESS_TOKEN/i.test(text)) {
783
+ manual.add('Supabase MCP uses SUPABASE_ACCESS_TOKEN but the variable is unset; export the token or migrate that server to a read-only remote URL.');
784
+ warnings.add('supabase_access_token_missing_observed');
785
+ blockers.add('supabase_access_token_missing_manual_auth_required');
786
+ }
787
+ if (/node_repl[\s\S]{0,500}No such file or directory|No such file or directory[\s\S]{0,500}node_repl/i.test(text)) {
788
+ warnings.add('node_repl_missing_command_observed');
789
+ }
790
+ return {
791
+ ...startupRepair,
792
+ ok: blockers.size === 0 && startupRepair?.ok !== false,
793
+ manual_actions: [...manual],
794
+ warnings: [...warnings],
795
+ blockers: [...blockers]
796
+ };
797
+ }
699
798
  //# sourceMappingURL=doctor.js.map
@@ -56,7 +56,7 @@ export async function syncCodexAgentRoles(input) {
56
56
  for (const role of DIRECTIVE_ROLES) {
57
57
  const file = path.join(targetDir, `${role}.toml`);
58
58
  const current = await fs.readFile(file, 'utf8').catch(() => '');
59
- if (current && !current.includes('SKS managed 3.1.4 directive role') && !current.includes('SKS managed 3.1.5 directive role') && !current.includes('SKS managed 3.1.6 directive role') && !current.includes('SKS managed 3.1.6 bounded role') && !current.includes('SKS managed 3.1.7 directive role'))
59
+ if (current && !isSksManagedDirectiveRole(current))
60
60
  continue;
61
61
  await writeTextAtomic(file, roleToml(role, rolePayloads[role]));
62
62
  created.push(file);
@@ -84,13 +84,9 @@ export async function syncCodexAgentRoles(input) {
84
84
  return report;
85
85
  }
86
86
  function roleToml(role, payload) {
87
- const strategyLine = payload?.strategy === 'agent_type'
88
- ? `agent_type = "${payload.agent_type || role}"`
89
- : `message_role_prefix = "${escapeToml(payload?.message_role_prefix || `Role: ${role}.`)}"`;
90
87
  return [
91
88
  `name = "${role}"`,
92
- `description = "SKS managed 3.1.7 directive role: ${role}"`,
93
- strategyLine,
89
+ `description = "SKS managed 3.1.11 directive role: ${role}"`,
94
90
  'model_reasoning_effort = "medium"',
95
91
  role.includes('implementer') ? 'sandbox_mode = "workspace-write"' : 'sandbox_mode = "read-only"',
96
92
  'approval_policy = "never"',
@@ -110,6 +106,10 @@ function roleToml(role, payload) {
110
106
  ''
111
107
  ].join('\n');
112
108
  }
109
+ function isSksManagedDirectiveRole(text) {
110
+ return /SKS managed 3\.1\.(?:4|5|6|7|11) (?:directive|bounded) role/.test(text)
111
+ || /\bmessage_role_prefix\s*=/.test(text) && /SKS managed 3\.1\./.test(text);
112
+ }
113
113
  function blockersOf(value) {
114
114
  return Boolean(value) && typeof value === 'object' && Array.isArray(value.blockers)
115
115
  ? (value.blockers).map((item) => String(item)).filter(Boolean)
@@ -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',