sneakoscope 0.7.24 → 0.7.25

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
@@ -95,7 +95,7 @@ sks bootstrap
95
95
 
96
96
  `sks` commands work even when no project root is present. Project-aware commands use the nearest `.sneakoscope`, `.dcodex`, or `.git` root; if none exists, SKS uses a per-user global runtime root. `sks bootstrap` still initializes the current project when you want project-local hooks, skills, and TriWiki state.
97
97
 
98
- Project setup writes shared `.gitignore` entries for generated SKS files: `.sneakoscope/`, `.codex/`, `.agents/`, and managed `AGENTS.md`. Use `sks setup --local-only` when you want those excludes kept only in `.git/info/exclude`.
98
+ Project setup writes shared `.gitignore` entries for generated SKS files: `.sneakoscope/`, `.codex/`, `.agents/`, and managed `AGENTS.md`. Setup, doctor repair, and npm postinstall refreshes also compare the previous SKS generated-file manifest with the current package templates and prune stale SKS-generated legacy skills or agent files while preserving user-owned custom skills. Use `sks setup --local-only` when you want those excludes kept only in `.git/info/exclude`.
99
99
 
100
100
  During npm postinstall, SKS also installs generated Codex App skills and tries the official getdesign Codex skill command, `skills add MohtashamMurshid/getdesign`, when the `skills` CLI is available. If that CLI is missing, setup still installs the generated `getdesign-reference` skill. Design work still flows through one authority: `design.md`. When `design.md` is missing, `docs/Design-Sys-Prompt.md` is the builder prompt and getdesign plus curated DESIGN.md examples such as [VoltAgent/awesome-design-md](https://github.com/VoltAgent/awesome-design-md) are inputs to fuse into that SSOT or into route-local `$PPT` style tokens.
101
101
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.24",
4
+ "version": "0.7.25",
5
5
  "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -186,7 +186,14 @@ export async function ensureGlobalCodexSkillsDuringInstall(opts = {}) {
186
186
  try {
187
187
  const install = await installSkills(home);
188
188
  const skills = await checkRequiredSkills(home, root);
189
- return { status: skills.ok ? 'installed' : 'partial', root, installed_count: install.installed_skills.length, removed_aliases: install.removed_agent_skill_aliases, missing_skills: skills.missing };
189
+ return {
190
+ status: skills.ok ? 'installed' : 'partial',
191
+ root,
192
+ installed_count: install.installed_skills.length,
193
+ removed_aliases: install.removed_agent_skill_aliases,
194
+ removed_stale_generated_skills: install.removed_stale_generated_skills,
195
+ missing_skills: skills.missing
196
+ };
190
197
  } catch (err) {
191
198
  return { status: 'failed', root, error: err.message };
192
199
  }
package/src/cli/main.mjs CHANGED
@@ -1396,11 +1396,12 @@ async function doctor(args) {
1396
1396
  let conflictScan = await scanHarnessConflicts(root);
1397
1397
  let repairApplied = false;
1398
1398
  let globalSkillsRepair = null;
1399
+ let projectRepair = null;
1399
1400
  const globalCommand = await globalSksCommand();
1400
1401
  if (flag(args, '--fix') && !conflictScan.hard_block) {
1401
1402
  const existingManifest = await readJson(path.join(root, '.sneakoscope', 'manifest.json'), null);
1402
1403
  const fixScope = requestedScope || normalizeInstallScope(existingManifest?.installation?.scope || 'global');
1403
- await initProject(root, { installScope: fixScope, globalCommand, localOnly: flag(args, '--local-only') || Boolean(existingManifest?.git?.local_only), force: true, repair: true });
1404
+ projectRepair = await initProject(root, { installScope: fixScope, globalCommand, localOnly: flag(args, '--local-only') || Boolean(existingManifest?.git?.local_only), force: true, repair: true });
1404
1405
  if (!flag(args, '--local-only')) globalSkillsRepair = await ensureGlobalCodexSkillsDuringInstall({ force: true });
1405
1406
  repairApplied = true;
1406
1407
  conflictScan = await scanHarnessConflicts(root);
@@ -1427,7 +1428,7 @@ async function doctor(args) {
1427
1428
  const result = {
1428
1429
  node: { ok: nodeOk, version: process.version }, root, codex, rust,
1429
1430
  install,
1430
- repair: { applied: repairApplied, global_skills: globalSkillsRepair, blocked_by_other_harness: flag(args, '--fix') && conflictScan.hard_block },
1431
+ repair: { applied: repairApplied, project: projectRepair, global_skills: globalSkillsRepair, blocked_by_other_harness: flag(args, '--fix') && conflictScan.hard_block },
1431
1432
  harness_conflicts: {
1432
1433
  ok: conflictScan.ok,
1433
1434
  hard_block: conflictScan.hard_block,
@@ -1743,6 +1744,26 @@ async function selftest() {
1743
1744
  await initProject(repairTmp, { installScope: 'project', localOnly: true });
1744
1745
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'), 'tampered\n');
1745
1746
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'), '---\nname: agent-team\ndescription: Fallback Codex App picker alias for $Team.\n---\n');
1747
+ await ensureDir(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated'));
1748
+ await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated', 'SKILL.md'), '---\nname: stale-sks-generated\ndescription: Old SKS generated skill that should disappear on update.\n---\n');
1749
+ await writeJsonAtomic(path.join(repairTmp, '.agents', 'skills', '.sks-generated.json'), {
1750
+ schema_version: 1,
1751
+ generated_by: 'sneakoscope',
1752
+ version: '0.0.1',
1753
+ skills: ['team', 'stale-sks-generated'],
1754
+ files: ['.agents/skills/team/SKILL.md', '.agents/skills/stale-sks-generated/SKILL.md']
1755
+ });
1756
+ const staleCodexAgentRel = '.codex/agents/stale-generated.toml';
1757
+ await writeTextAtomic(path.join(repairTmp, staleCodexAgentRel), 'name = "stale_generated"\n');
1758
+ const staleManifest = await readJson(path.join(repairTmp, '.sneakoscope', 'manifest.json'));
1759
+ staleManifest.version = '0.0.1';
1760
+ staleManifest.generated_files = {
1761
+ schema_version: 1,
1762
+ generated_by: 'sneakoscope',
1763
+ prune_policy: 'remove_previous_sks_generated_paths_absent_from_current_manifest',
1764
+ files: [...(staleManifest.generated_files?.files || []), '.agents/skills/stale-sks-generated/SKILL.md', staleCodexAgentRel]
1765
+ };
1766
+ await writeJsonAtomic(path.join(repairTmp, '.sneakoscope', 'manifest.json'), staleManifest);
1746
1767
  await ensureDir(path.join(repairTmp, '.agents', 'skills', 'custom-keep'));
1747
1768
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'custom-keep', 'SKILL.md'), '---\nname: custom-keep\ndescription: User custom skill, not generated by SKS.\n---\n');
1748
1769
  await writeTextAtomic(path.join(repairTmp, '.codex', 'skills', 'team', 'SKILL.md'), 'legacy mirror\n');
@@ -1765,6 +1786,11 @@ async function selftest() {
1765
1786
  const repairedTeamSkill = await safeReadText(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'));
1766
1787
  if (!repairedTeamSkill.includes('SKS Team orchestration') || repairedTeamSkill.includes('tampered')) throw new Error('selftest failed: doctor repair did not regenerate team skill');
1767
1788
  if (await exists(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest failed: doctor repair did not remove deprecated agent-team alias skill');
1789
+ if (await exists(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated', 'SKILL.md'))) throw new Error('selftest failed: doctor repair did not prune stale generated skill from previous SKS manifest');
1790
+ if (await exists(path.join(repairTmp, staleCodexAgentRel))) throw new Error('selftest failed: doctor repair did not prune stale generated agent file from previous SKS manifest');
1791
+ if (!doctorRepairJson.repair?.project?.skill_install?.removed_stale_generated_skills?.includes('.agents/skills/stale-sks-generated')) throw new Error('selftest failed: doctor repair did not report stale generated skill pruning');
1792
+ const generatedCleanupReport = doctorRepairJson.repair?.project?.generated_cleanup || {};
1793
+ if (![...(generatedCleanupReport.pruned || []), ...(generatedCleanupReport.already_absent || [])].includes(staleCodexAgentRel)) throw new Error('selftest failed: doctor repair did not report stale generated file pruning');
1768
1794
  if (!(await exists(path.join(repairTmp, '.agents', 'skills', 'custom-keep', 'SKILL.md')))) throw new Error('selftest failed: doctor repair removed a user-owned custom skill');
1769
1795
  if (await exists(path.join(repairTmp, '.codex', 'skills', 'team', 'SKILL.md'))) throw new Error('selftest failed: doctor repair did not remove legacy .codex/skills');
1770
1796
  const repairedQuickReference = await safeReadText(path.join(repairTmp, '.codex', 'SNEAKOSCOPE.md'));
package/src/core/fsx.mjs 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
 
8
- export const PACKAGE_VERSION = '0.7.24';
8
+ export const PACKAGE_VERSION = '0.7.25';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
package/src/core/init.mjs CHANGED
@@ -11,6 +11,8 @@ import { SKILL_DREAM_POLICY, skillDreamPolicyText } from './skill-forge.mjs';
11
11
 
12
12
  const REFLECTION_MEMORY_PATH = '.sneakoscope/memory/q2_facts/post-route-reflection.md';
13
13
  const SKS_GENERATED_GIT_PATTERNS = ['.sneakoscope/', '.codex/', '.agents/', 'AGENTS.md'];
14
+ const SKS_SKILL_MANIFEST_FILE = '.sks-generated.json';
15
+ const GENERATED_PRUNE_POLICY = 'remove_previous_sks_generated_paths_absent_from_current_manifest';
14
16
 
15
17
  function reflectionInstructionText(commandPrefix = 'sks') {
16
18
  return `Post-route reflection: full routes load \`reflection\` after work/tests and before final; DFix/Answer/Help/Wiki/SKS discovery are exempt. Write reflection.md; record only real misses/gaps, or no_issue_acknowledged. For lessons, append TriWiki claim rows to ${REFLECTION_MEMORY_PATH}. Run "${commandPrefix} wiki refresh" or pack, validate, then pass reflection-gate.json.`;
@@ -101,6 +103,8 @@ export async function initProject(root, opts = {}) {
101
103
  const requestedHookCommandPrefix = opts.hookCommandPrefix || sksCommandPrefix(installScope, { globalCommand: opts.globalCommand });
102
104
  const hookCommandPrefix = sourceProject ? 'node ./bin/sks.mjs' : requestedHookCommandPrefix;
103
105
  const sine = path.join(root, '.sneakoscope');
106
+ const manifestPath = path.join(sine, 'manifest.json');
107
+ const previousManifest = await readJson(manifestPath, null);
104
108
  if (opts.repair) {
105
109
  const repair = await repairSksGeneratedArtifacts(root, { resetState: Boolean(opts.resetState) });
106
110
  if (repair.removed.length) created.push(`repaired generated SKS files (${repair.removed.length})`);
@@ -114,7 +118,7 @@ export async function initProject(root, opts = {}) {
114
118
  if (localExclude?.path) created.push(`${path.relative(root, localExclude.path)} local-only excludes`);
115
119
  if (sharedIgnore?.changed) created.push(`${path.relative(root, sharedIgnore.path)} SKS generated files ignore`);
116
120
 
117
- await writeJsonAtomic(path.join(sine, 'manifest.json'), {
121
+ const manifest = {
118
122
  package: 'sneakoscope',
119
123
  version: PACKAGE_VERSION,
120
124
  initialized_at: nowIso(),
@@ -197,7 +201,8 @@ export async function initProject(root, opts = {}) {
197
201
  },
198
202
  database_safety: 'destructive_db_operations_denied_always',
199
203
  gx_renderer: 'deterministic_svg_html'
200
- });
204
+ };
205
+ await writeJsonAtomic(manifestPath, manifest);
201
206
  created.push('.sneakoscope/manifest.json');
202
207
 
203
208
  const dbSafetyPath = path.join(sine, 'db-safety.json');
@@ -467,16 +472,36 @@ policy = "Deny destructive database operations, credential exfiltration, persist
467
472
 
468
473
  const skillInstall = await installSkills(root);
469
474
  created.push('.agents/skills/*');
475
+ if (skillInstall.removed_stale_generated_skills.length) created.push(`stale generated skills removed (${skillInstall.removed_stale_generated_skills.length})`);
476
+ if (skillInstall.removed_agent_skill_aliases.length) created.push(`deprecated generated skill aliases removed (${skillInstall.removed_agent_skill_aliases.length})`);
470
477
  if (skillInstall.removed_codex_skill_mirrors.length) created.push(`.codex/skills generated mirrors removed (${skillInstall.removed_codex_skill_mirrors.length})`);
471
- await installCodexAgents(root);
478
+ const agentInstall = await installCodexAgents(root);
472
479
  created.push('.codex/agents/*');
480
+ const generatedFiles = currentGeneratedFileInventory(skillInstall, agentInstall);
481
+ const generatedCleanup = await pruneStaleGeneratedFiles(root, previousManifest, generatedFiles);
482
+ if (generatedCleanup.pruned.length) created.push(`stale generated files pruned (${generatedCleanup.pruned.length})`);
483
+ manifest.generated_files = {
484
+ schema_version: 1,
485
+ generated_by: 'sneakoscope',
486
+ prune_policy: GENERATED_PRUNE_POLICY,
487
+ files: generatedFiles
488
+ };
489
+ manifest.generated_cleanup = {
490
+ schema_version: 1,
491
+ last_run_at: nowIso(),
492
+ previous_version: previousManifest?.version || null,
493
+ current_version: PACKAGE_VERSION,
494
+ pruned: generatedCleanup.pruned,
495
+ already_absent: generatedCleanup.already_absent || []
496
+ };
497
+ await writeJsonAtomic(manifestPath, manifest);
473
498
  await writeHarnessGuardPolicy(root);
474
499
  created.push('.sneakoscope/harness-guard.json');
475
500
  const versionHookCommand = sourceProject ? 'node ./bin/sks.mjs' : hookCommandPrefix;
476
501
  const versionHook = await installVersionGitHook(root, versionHookCommand);
477
502
  if (versionHook.installed) created.push('.git/hooks/pre-commit SKS version guard');
478
503
  else created.push(`version guard skipped (${versionHook.reason})`);
479
- return { created };
504
+ return { created, generated_cleanup: generatedCleanup, skill_install: skillInstall };
480
505
  }
481
506
 
482
507
  async function ensureSharedGitIgnore(root) {
@@ -601,13 +626,57 @@ export async function installSkills(root) {
601
626
  await writeSkillMetadata(dir, name);
602
627
  }
603
628
  const skillNames = Object.keys(skills);
629
+ const removedStaleGeneratedSkills = await removeStaleGeneratedSkillsFromManifest(root, skillNames);
630
+ await writeGeneratedSkillManifest(root, skillNames);
604
631
  return {
605
632
  installed_skills: skillNames,
633
+ generated_files: generatedSkillFiles(skillNames),
634
+ removed_stale_generated_skills: removedStaleGeneratedSkills,
606
635
  removed_agent_skill_aliases: await removeGeneratedAgentSkillAliases(root, skillNames),
607
636
  removed_codex_skill_mirrors: await removeGeneratedCodexSkillMirrors(root, skillNames)
608
637
  };
609
638
  }
610
639
 
640
+ function generatedSkillFiles(skillNames) {
641
+ return skillNames.flatMap((name) => [
642
+ `.agents/skills/${name}/SKILL.md`,
643
+ `.agents/skills/${name}/agents/openai.yaml`
644
+ ]).sort();
645
+ }
646
+
647
+ function generatedSkillManifestPath(root) {
648
+ return path.join(root, '.agents', 'skills', SKS_SKILL_MANIFEST_FILE);
649
+ }
650
+
651
+ async function writeGeneratedSkillManifest(root, skillNames) {
652
+ const manifestPath = generatedSkillManifestPath(root);
653
+ await writeJsonAtomic(manifestPath, {
654
+ schema_version: 1,
655
+ generated_by: 'sneakoscope',
656
+ version: PACKAGE_VERSION,
657
+ prune_policy: GENERATED_PRUNE_POLICY,
658
+ skills: [...skillNames].sort(),
659
+ files: generatedSkillFiles(skillNames)
660
+ });
661
+ }
662
+
663
+ async function removeStaleGeneratedSkillsFromManifest(root, skillNames) {
664
+ const previous = await readJson(generatedSkillManifestPath(root), null);
665
+ const previousSkills = Array.isArray(previous?.skills) ? previous.skills : [];
666
+ if (!previousSkills.length) return [];
667
+ const current = new Set(skillNames);
668
+ const removed = [];
669
+ for (const name of previousSkills) {
670
+ const skillName = String(name || '').trim();
671
+ if (!skillName || current.has(skillName) || !/^[a-z0-9-]+$/.test(skillName)) continue;
672
+ const dir = path.join(root, '.agents', 'skills', skillName);
673
+ if (!(await exists(dir))) continue;
674
+ await fsp.rm(dir, { recursive: true, force: true });
675
+ removed.push(path.relative(root, dir));
676
+ }
677
+ return removed.sort();
678
+ }
679
+
611
680
  function enrichSkillContent(name, content) {
612
681
  if (!['sks', 'answer', 'wiki', 'team', 'qa-loop', 'ppt', 'computer-use', 'computer-use-fast', 'cu', 'goal', 'research', 'autoresearch', 'db', 'gx', 'reflection', 'prompt-pipeline', 'pipeline-runner', 'context7-docs', 'turbo-context-pack', 'hproof-evidence-bind'].includes(name)) return content;
613
682
  const text = String(content || '').trimEnd();
@@ -704,4 +773,89 @@ async function installCodexAgents(root) {
704
773
  for (const [file, content] of Object.entries(agents)) {
705
774
  await writeTextAtomic(path.join(dir, file), content);
706
775
  }
776
+ return {
777
+ installed_agents: Object.keys(agents),
778
+ generated_files: Object.keys(agents).map((file) => `.codex/agents/${file}`).sort()
779
+ };
780
+ }
781
+
782
+ function currentGeneratedFileInventory(skillInstall = {}, agentInstall = {}) {
783
+ return Array.from(new Set([
784
+ '.codex/config.toml',
785
+ '.codex/SNEAKOSCOPE.md',
786
+ '.codex/hooks.json',
787
+ '.sneakoscope/harness-guard.json',
788
+ '.sneakoscope/db-safety.json',
789
+ '.sneakoscope/policy.json',
790
+ '.agents/skills/.sks-generated.json',
791
+ ...(Array.isArray(skillInstall.generated_files) ? skillInstall.generated_files : []),
792
+ ...(Array.isArray(agentInstall.generated_files) ? agentInstall.generated_files : [])
793
+ ])).sort();
794
+ }
795
+
796
+ async function pruneStaleGeneratedFiles(root, previousManifest, currentFiles) {
797
+ const previousFiles = Array.isArray(previousManifest?.generated_files?.files) ? previousManifest.generated_files.files : [];
798
+ if (!previousFiles.length) return { pruned: [] };
799
+ const current = new Set(currentFiles);
800
+ const pruned = [];
801
+ const already_absent = [];
802
+ for (const rel of previousFiles) {
803
+ const relPath = normalizeGeneratedRelPath(rel);
804
+ if (!relPath || current.has(relPath) || !isPrunableGeneratedPath(relPath)) continue;
805
+ const removed = await removeGeneratedRelPath(root, relPath);
806
+ if (removed) pruned.push(removed);
807
+ else already_absent.push(relPath);
808
+ }
809
+ return { pruned: pruned.sort(), already_absent: already_absent.sort() };
810
+ }
811
+
812
+ function normalizeGeneratedRelPath(value) {
813
+ const rel = String(value || '').trim().replaceAll('\\', '/');
814
+ if (!rel || rel.startsWith('/') || rel.includes('\0')) return null;
815
+ if (rel.split('/').some((part) => part === '..')) return null;
816
+ return rel;
817
+ }
818
+
819
+ function isPrunableGeneratedPath(rel) {
820
+ if (rel.startsWith('.agents/skills/')) return true;
821
+ if (rel.startsWith('.codex/agents/')) return true;
822
+ if (rel.startsWith('.codex/skills/')) return true;
823
+ return new Set([
824
+ '.codex/config.toml',
825
+ '.codex/SNEAKOSCOPE.md',
826
+ '.codex/hooks.json',
827
+ '.sneakoscope/harness-guard.json',
828
+ '.sneakoscope/db-safety.json',
829
+ '.sneakoscope/policy.json'
830
+ ]).has(rel);
831
+ }
832
+
833
+ async function removeGeneratedRelPath(root, rel) {
834
+ const absRoot = path.resolve(root);
835
+ const abs = path.resolve(absRoot, rel);
836
+ if (abs !== absRoot && !abs.startsWith(`${absRoot}${path.sep}`)) return null;
837
+ if (!(await exists(abs))) return null;
838
+ await fsp.rm(abs, { recursive: true, force: true });
839
+ await removeEmptyGeneratedParents(root, rel);
840
+ return rel;
841
+ }
842
+
843
+ async function removeEmptyGeneratedParents(root, rel) {
844
+ const parts = rel.split('/');
845
+ if (parts.length <= 1) return;
846
+ const stopDirs = new Set([
847
+ path.resolve(root, '.agents', 'skills'),
848
+ path.resolve(root, '.codex', 'agents'),
849
+ path.resolve(root, '.codex', 'skills'),
850
+ path.resolve(root, '.codex'),
851
+ path.resolve(root, '.sneakoscope')
852
+ ]);
853
+ let dir = path.resolve(root, ...parts.slice(0, -1));
854
+ while (!stopDirs.has(dir) && dir.startsWith(path.resolve(root))) {
855
+ await removeDirIfEmpty(dir);
856
+ const parent = path.dirname(dir);
857
+ if (parent === dir) break;
858
+ dir = parent;
859
+ }
860
+ if (rel.startsWith('.codex/skills/')) await removeDirIfEmpty(path.join(root, '.codex', 'skills'));
707
861
  }