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 +1 -1
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +8 -1
- package/src/cli/main.mjs +28 -2
- package/src/core/fsx.mjs +1 -1
- package/src/core/init.mjs +158 -4
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.
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
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
|
}
|