sneakoscope 3.1.7 → 3.1.9
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 +9 -2
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/cli/args.js +17 -0
- package/dist/cli/command-registry.js +16 -13
- package/dist/cli/router.js +8 -5
- package/dist/commands/doctor.js +97 -2
- package/dist/core/codex-app/codex-skill-sync.js +80 -154
- package/dist/core/codex-native/core-skill-integrity.js +89 -0
- package/dist/core/codex-native/core-skill-manifest.js +156 -0
- package/dist/core/codex-native/native-capability-postcheck.js +35 -0
- package/dist/core/codex-native/native-capability-repair-matrix.js +210 -0
- package/dist/core/codex-native/native-capability-repair.js +47 -0
- package/dist/core/codex-native/native-media-computer-repair.js +5 -0
- package/dist/core/codex-native/project-skill-dedupe.js +109 -0
- package/dist/core/codex-native/skill-name-canonicalizer.js +21 -0
- package/dist/core/codex-native/skill-registry-ledger.js +85 -0
- package/dist/core/commands/basic-cli.js +19 -10
- package/dist/core/commands/mad-sks-command.js +36 -13
- package/dist/core/commands/naruto-command.js +4 -1
- package/dist/core/commands/pipeline-command.js +3 -4
- package/dist/core/commands/qa-loop-command.js +36 -1
- package/dist/core/commands/research-command.js +61 -1
- package/dist/core/commands/team-command.js +63 -3
- package/dist/core/config/config-migration-journal.js +27 -0
- package/dist/core/config/managed-config-merge.js +105 -0
- package/dist/core/config/secret-preservation.js +169 -0
- package/dist/core/config/supabase-secret-preservation.js +29 -0
- package/dist/core/decision-contract.js +28 -4
- package/dist/core/doctor/command-alias-cleanup.js +64 -0
- package/dist/core/doctor/doctor-native-capability-repair.js +48 -0
- package/dist/core/feature-fixtures.js +2 -0
- package/dist/core/feature-registry.js +2 -2
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +5 -1
- package/dist/core/naruto/naruto-work-graph.js +4 -1
- package/dist/core/pipeline-internals/runtime-core.js +50 -4
- package/dist/core/pipeline-internals/runtime-gates.js +10 -1
- package/dist/core/proof/route-proof-gate.js +1 -1
- package/dist/core/qa-loop.js +227 -11
- package/dist/core/questions.js +239 -2
- package/dist/core/routes.js +3 -4
- package/dist/core/version.js +1 -1
- package/dist/scripts/agent-native-release-gate.js +13 -4
- package/dist/scripts/sizecheck.js +8 -2
- package/dist/scripts/sks-3-1-8-check-lib.js +30 -0
- package/package.json +27 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ensureDir, nowIso, readText, sha256, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
|
|
4
|
+
import { buildSksCoreSkillManifest, isSksManagedCoreSkillContent, renderCoreSkillTemplate } from './core-skill-manifest.js';
|
|
5
|
+
import { canonicalSkillName } from './skill-name-canonicalizer.js';
|
|
6
|
+
export async function syncCoreSkillsIntegrity(input) {
|
|
7
|
+
const root = path.resolve(input.root);
|
|
8
|
+
const skillsRoot = input.skillsRoot || path.join(root, '.agents', 'skills');
|
|
9
|
+
const apply = input.apply === true;
|
|
10
|
+
const manifest = buildSksCoreSkillManifest();
|
|
11
|
+
const rows = [];
|
|
12
|
+
const installed = [];
|
|
13
|
+
const restored = [];
|
|
14
|
+
const skippedUserAuthored = [];
|
|
15
|
+
const blockers = [];
|
|
16
|
+
for (const skill of manifest.skills) {
|
|
17
|
+
const skillDir = path.join(skillsRoot, skill.canonical_name);
|
|
18
|
+
const file = path.join(skillDir, 'SKILL.md');
|
|
19
|
+
const desired = renderCoreSkillTemplate(skill.canonical_name);
|
|
20
|
+
const current = await readText(file, null);
|
|
21
|
+
const beforeSha = typeof current === 'string' ? sha256(current) : null;
|
|
22
|
+
let action = 'already-current';
|
|
23
|
+
let backupPath = null;
|
|
24
|
+
let blocker = null;
|
|
25
|
+
if (current === null) {
|
|
26
|
+
action = 'install-missing-managed-copy';
|
|
27
|
+
if (apply) {
|
|
28
|
+
await ensureDir(skillDir);
|
|
29
|
+
await writeTextAtomic(file, desired);
|
|
30
|
+
installed.push(file);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (beforeSha === skill.content_sha256) {
|
|
34
|
+
action = 'already-current';
|
|
35
|
+
}
|
|
36
|
+
else if (isSksManagedCoreSkillContent(current)) {
|
|
37
|
+
action = 'restore-corrupted-managed-copy';
|
|
38
|
+
if (apply) {
|
|
39
|
+
backupPath = path.join(root, '.sneakoscope', 'backups', 'core-skills', skill.canonical_name, `${Date.now()}-${process.pid}.SKILL.md.bak`);
|
|
40
|
+
await ensureDir(path.dirname(backupPath));
|
|
41
|
+
await fs.writeFile(backupPath, current, 'utf8');
|
|
42
|
+
await writeTextAtomic(file, desired);
|
|
43
|
+
restored.push(file);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
action = 'skip-user-authored';
|
|
48
|
+
blocker = `user_authored_core_skill_collision:${skill.canonical_name}`;
|
|
49
|
+
skippedUserAuthored.push(file);
|
|
50
|
+
}
|
|
51
|
+
const after = await readText(file, null);
|
|
52
|
+
const afterSha = typeof after === 'string' ? sha256(after) : null;
|
|
53
|
+
rows.push({
|
|
54
|
+
canonical_name: skill.canonical_name,
|
|
55
|
+
path: file,
|
|
56
|
+
action,
|
|
57
|
+
before_sha256: beforeSha,
|
|
58
|
+
after_sha256: afterSha,
|
|
59
|
+
backup_path: backupPath,
|
|
60
|
+
blocker
|
|
61
|
+
});
|
|
62
|
+
if (blocker)
|
|
63
|
+
blockers.push(blocker);
|
|
64
|
+
}
|
|
65
|
+
const report = {
|
|
66
|
+
schema: 'sks.core-skill-integrity.v1',
|
|
67
|
+
generated_at: nowIso(),
|
|
68
|
+
ok: blockers.length === 0,
|
|
69
|
+
root,
|
|
70
|
+
apply,
|
|
71
|
+
skills_root: skillsRoot,
|
|
72
|
+
manifest_sha256: sha256(JSON.stringify(manifest.skills.map((skill) => [skill.canonical_name, skill.content_sha256]))),
|
|
73
|
+
rows,
|
|
74
|
+
installed,
|
|
75
|
+
restored,
|
|
76
|
+
skipped_user_authored: skippedUserAuthored,
|
|
77
|
+
blockers
|
|
78
|
+
};
|
|
79
|
+
const reportPath = input.reportPath === null
|
|
80
|
+
? null
|
|
81
|
+
: input.reportPath || path.join(root, '.sneakoscope', 'reports', 'core-skill-integrity.json');
|
|
82
|
+
if (reportPath)
|
|
83
|
+
await writeJsonAtomic(reportPath, report).catch(() => undefined);
|
|
84
|
+
return report;
|
|
85
|
+
}
|
|
86
|
+
export function coreSkillPath(skillsRoot, name) {
|
|
87
|
+
return path.join(skillsRoot, canonicalSkillName(name), 'SKILL.md');
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=core-skill-integrity.js.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { PACKAGE_VERSION, nowIso, sha256 } from '../fsx.js';
|
|
2
|
+
import { canonicalSkillName } from './skill-name-canonicalizer.js';
|
|
3
|
+
export const CORE_SKILL_TEMPLATE_VERSION = '3.1.8-core-skill-template.v1';
|
|
4
|
+
export const CORE_SKILL_MANAGED_BEGIN = '<!-- BEGIN SKS IMMUTABLE CORE SKILL -->';
|
|
5
|
+
export const CORE_SKILL_MANAGED_END = '<!-- END SKS IMMUTABLE CORE SKILL -->';
|
|
6
|
+
const CORE_SKILL_DEFINITIONS = [
|
|
7
|
+
{
|
|
8
|
+
id: 'sks-core-loop',
|
|
9
|
+
canonical_name: 'loop',
|
|
10
|
+
display_name: 'loop',
|
|
11
|
+
route: '$Loop',
|
|
12
|
+
purpose: 'compile persisted route work into bounded loop plans with continuation evidence.',
|
|
13
|
+
when: 'Use for resumable route stages, memory hints, and loop mission artifacts.',
|
|
14
|
+
evidence: '.sneakoscope/loops/** plus route-local proof artifacts.',
|
|
15
|
+
fallback: 'Record the unavailable surface as blocked; do not fabricate a loop proof.'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'sks-core-naruto',
|
|
19
|
+
canonical_name: 'naruto',
|
|
20
|
+
display_name: 'naruto',
|
|
21
|
+
route: '$Naruto',
|
|
22
|
+
purpose: 'fan out bounded native worker lanes while parent integration remains owner.',
|
|
23
|
+
when: 'Use when the selected route explicitly requires high-scale parallel review or implementation.',
|
|
24
|
+
evidence: 'agent task graph, worker ledgers, leases, proof evidence, and cleanup artifacts.',
|
|
25
|
+
fallback: 'Degrade to parent-owned execution with blockers recorded if native lanes are unavailable.'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'sks-core-qa-loop',
|
|
29
|
+
canonical_name: 'qa-loop',
|
|
30
|
+
display_name: 'qa-loop',
|
|
31
|
+
route: '$QA-LOOP',
|
|
32
|
+
purpose: 'dogfood UI/API behavior with safety gates and QA reports.',
|
|
33
|
+
when: 'Use when route completion needs human-proxy verification, rechecks, and QA ledgers.',
|
|
34
|
+
evidence: 'qa-ledger.json, dated QA report, qa-gate.json, and post-fix verification.',
|
|
35
|
+
fallback: 'Mark unverified browser/native surfaces explicitly; never substitute fake visual evidence.'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'sks-core-research',
|
|
39
|
+
canonical_name: 'research',
|
|
40
|
+
display_name: 'research',
|
|
41
|
+
route: '$Research',
|
|
42
|
+
purpose: 'run evidence-bound discovery, source ledgers, and synthesis cycles.',
|
|
43
|
+
when: 'Use for discovery, evaluation, external-source claims, or frontier-style research.',
|
|
44
|
+
evidence: 'research plan, source ledger, cycle record, synthesis, and final review.',
|
|
45
|
+
fallback: 'State source/tool unavailability and avoid unsupported live-accuracy claims.'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'sks-core-dfix',
|
|
49
|
+
canonical_name: 'dfix',
|
|
50
|
+
display_name: 'dfix',
|
|
51
|
+
route: '$DFix',
|
|
52
|
+
purpose: 'perform tiny direct fixes with cheap verification.',
|
|
53
|
+
when: 'Use only for narrow copy/config/docs/labels/spacing/translation/mechanical edits.',
|
|
54
|
+
evidence: 'focused diff and DFix Honest check.',
|
|
55
|
+
fallback: 'Escalate broad implementation to a full execution route.'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'sks-core-image-ux-review',
|
|
59
|
+
canonical_name: 'image-ux-review',
|
|
60
|
+
display_name: 'image-ux-review',
|
|
61
|
+
route: '$Image-UX-Review',
|
|
62
|
+
purpose: 'produce generated annotated UI review images and extract issue ledgers.',
|
|
63
|
+
when: 'Use for screenshot/UI UX review requests that require generated raster evidence.',
|
|
64
|
+
evidence: 'source inventory, generated annotation image ledger, issue ledger, iteration report.',
|
|
65
|
+
fallback: 'Block full verification if generated annotated images cannot be produced.'
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'sks-core-computer-use',
|
|
69
|
+
canonical_name: 'computer-use',
|
|
70
|
+
display_name: 'computer-use',
|
|
71
|
+
route: '$Computer-Use',
|
|
72
|
+
purpose: 'operate native macOS desktop apps through Codex Computer Use.',
|
|
73
|
+
when: 'Use only for native Mac/non-web app or OS-setting surfaces.',
|
|
74
|
+
evidence: 'native desktop interaction evidence where live Computer Use is available.',
|
|
75
|
+
fallback: 'Do not use Computer Use as browser/web evidence; mark unavailable surfaces unverified.'
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'sks-core-init-deep',
|
|
79
|
+
canonical_name: 'init-deep',
|
|
80
|
+
display_name: 'init-deep',
|
|
81
|
+
route: '$Init-Deep',
|
|
82
|
+
purpose: 'refresh project-local memory, directory rules, and loop memory hints.',
|
|
83
|
+
when: 'Use when deeper local context or directory-specific recall is required.',
|
|
84
|
+
evidence: '.sneakoscope/context/AGENTS.generated.md and managed memory artifacts.',
|
|
85
|
+
fallback: 'Preserve user content and skip directories that cannot be safely updated.'
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
export function coreSkillDefinitions() {
|
|
89
|
+
return CORE_SKILL_DEFINITIONS;
|
|
90
|
+
}
|
|
91
|
+
export function isCoreSkillName(name) {
|
|
92
|
+
const canonical = canonicalSkillName(name);
|
|
93
|
+
return CORE_SKILL_DEFINITIONS.some((skill) => skill.canonical_name === canonical);
|
|
94
|
+
}
|
|
95
|
+
export function renderCoreSkillTemplate(name) {
|
|
96
|
+
const canonical = canonicalSkillName(name);
|
|
97
|
+
const skill = CORE_SKILL_DEFINITIONS.find((entry) => entry.canonical_name === canonical);
|
|
98
|
+
if (!skill)
|
|
99
|
+
throw new Error(`Unknown SKS core skill: ${name}`);
|
|
100
|
+
return [
|
|
101
|
+
'---',
|
|
102
|
+
`name: ${skill.display_name}`,
|
|
103
|
+
`description: Immutable SKS core Codex App route bridge for ${skill.route}.`,
|
|
104
|
+
'---',
|
|
105
|
+
'',
|
|
106
|
+
CORE_SKILL_MANAGED_BEGIN,
|
|
107
|
+
`id: ${skill.id}`,
|
|
108
|
+
`canonical_name: ${skill.canonical_name}`,
|
|
109
|
+
`route: ${skill.route}`,
|
|
110
|
+
`template_version: ${CORE_SKILL_TEMPLATE_VERSION}`,
|
|
111
|
+
'mutable_by_doctor: false',
|
|
112
|
+
'mutable_by_update: false',
|
|
113
|
+
'mutable_by_setup: false',
|
|
114
|
+
CORE_SKILL_MANAGED_END,
|
|
115
|
+
'',
|
|
116
|
+
`Route: ${skill.route}`,
|
|
117
|
+
`Command: ${skill.route}`,
|
|
118
|
+
`Purpose: ${skill.purpose}`,
|
|
119
|
+
`Use when: ${skill.when}`,
|
|
120
|
+
`Proof paths: ${skill.evidence}`,
|
|
121
|
+
'Safety rules: preserve user-authored skills, keep route state bounded, and stop on hard blockers instead of fabricating fallback behavior.',
|
|
122
|
+
`Failure recovery: ${skill.fallback}`,
|
|
123
|
+
''
|
|
124
|
+
].join('\n');
|
|
125
|
+
}
|
|
126
|
+
export function buildSksCoreSkillManifest(generatedAt = nowIso()) {
|
|
127
|
+
return {
|
|
128
|
+
schema: 'sks.core-skill-manifest.v1',
|
|
129
|
+
generated_at: generatedAt,
|
|
130
|
+
package_version: PACKAGE_VERSION,
|
|
131
|
+
skills: CORE_SKILL_DEFINITIONS.map((skill) => {
|
|
132
|
+
const content = renderCoreSkillTemplate(skill.canonical_name);
|
|
133
|
+
return {
|
|
134
|
+
id: skill.id,
|
|
135
|
+
canonical_name: skill.canonical_name,
|
|
136
|
+
display_name: skill.display_name,
|
|
137
|
+
route: skill.route,
|
|
138
|
+
relative_path: `.agents/skills/${skill.canonical_name}/SKILL.md`,
|
|
139
|
+
template_version: CORE_SKILL_TEMPLATE_VERSION,
|
|
140
|
+
content_sha256: sha256(content),
|
|
141
|
+
mutable_by_doctor: false,
|
|
142
|
+
mutable_by_update: false,
|
|
143
|
+
mutable_by_setup: false
|
|
144
|
+
};
|
|
145
|
+
})
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function coreSkillTemplateByCanonicalName(name) {
|
|
149
|
+
const canonical = canonicalSkillName(name);
|
|
150
|
+
return buildSksCoreSkillManifest('1970-01-01T00:00:00.000Z').skills.find((skill) => skill.canonical_name === canonical) || null;
|
|
151
|
+
}
|
|
152
|
+
export function isSksManagedCoreSkillContent(text) {
|
|
153
|
+
const value = String(text || '');
|
|
154
|
+
return value.includes(CORE_SKILL_MANAGED_BEGIN) && value.includes(CORE_SKILL_MANAGED_END);
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=core-skill-manifest.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { buildNativeCapabilityRepairMatrix } from './native-capability-repair-matrix.js';
|
|
4
|
+
export async function postcheckNativeCapabilities(input) {
|
|
5
|
+
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);
|
|
20
|
+
const checked = {
|
|
21
|
+
...matrix,
|
|
22
|
+
generated_at: new Date().toISOString(),
|
|
23
|
+
ok: blockers.length === 0,
|
|
24
|
+
capabilities,
|
|
25
|
+
blockers,
|
|
26
|
+
warnings: capabilities.flatMap((state) => state.warnings)
|
|
27
|
+
};
|
|
28
|
+
const reportPath = input.reportPath === null
|
|
29
|
+
? null
|
|
30
|
+
: input.reportPath || path.join(root, '.sneakoscope', 'reports', 'native-capability-postcheck.json');
|
|
31
|
+
if (reportPath)
|
|
32
|
+
await writeJsonAtomic(reportPath, checked).catch(() => undefined);
|
|
33
|
+
return checked;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=native-capability-postcheck.js.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ensureDir, nowIso, writeJsonAtomic } from '../fsx.js';
|
|
4
|
+
import { detectImagegenCapability } from '../imagegen/imagegen-capability.js';
|
|
5
|
+
import { buildCodexNativeFeatureMatrix } from './codex-native-feature-broker.js';
|
|
6
|
+
export const NATIVE_CAPABILITY_IDS = [
|
|
7
|
+
'image_generation',
|
|
8
|
+
'image_followup_edit',
|
|
9
|
+
'computer_use',
|
|
10
|
+
'chrome_web_review',
|
|
11
|
+
'codex_app_screenshot',
|
|
12
|
+
'app_handoff',
|
|
13
|
+
'image_path_exposure',
|
|
14
|
+
'saved_artifact_path_contract'
|
|
15
|
+
];
|
|
16
|
+
export async function buildNativeCapabilityRepairMatrix(input) {
|
|
17
|
+
const root = path.resolve(input.root);
|
|
18
|
+
const selected = new Set(input.capabilities || NATIVE_CAPABILITY_IDS);
|
|
19
|
+
const fixture = input.fixture || false;
|
|
20
|
+
const imageCapability = fixture
|
|
21
|
+
? fixtureImageCapability(fixture)
|
|
22
|
+
: await detectImagegenCapability({ timeoutMs: 2500 }).catch((err) => ({ blockers: [messageOf(err)], auth_readiness: null, codex_app: { available: false } }));
|
|
23
|
+
const nativeFeatureMatrix = fixture
|
|
24
|
+
? fixtureNativeFeatureMatrix(fixture)
|
|
25
|
+
: await buildCodexNativeFeatureMatrix({ root, mode: 'read-only' }).catch((err) => ({ ok: false, features: {}, blockers: [messageOf(err)], invocation_defaults: {} }));
|
|
26
|
+
const states = await Promise.all(NATIVE_CAPABILITY_IDS.filter((id) => selected.has(id)).map((id) => stateForCapability(root, id, imageCapability, nativeFeatureMatrix)));
|
|
27
|
+
const blockers = states.flatMap((state) => state.blockers);
|
|
28
|
+
const warnings = states.flatMap((state) => state.warnings);
|
|
29
|
+
const matrix = {
|
|
30
|
+
schema: 'sks.native-capability-repair-matrix.v1',
|
|
31
|
+
generated_at: nowIso(),
|
|
32
|
+
ok: blockers.length === 0,
|
|
33
|
+
capabilities: states,
|
|
34
|
+
blockers,
|
|
35
|
+
warnings
|
|
36
|
+
};
|
|
37
|
+
const reportPath = input.reportPath === null
|
|
38
|
+
? null
|
|
39
|
+
: input.reportPath || path.join(root, '.sneakoscope', 'reports', 'native-capability-repair-matrix.json');
|
|
40
|
+
if (reportPath)
|
|
41
|
+
await writeJsonAtomic(reportPath, matrix).catch(() => undefined);
|
|
42
|
+
return matrix;
|
|
43
|
+
}
|
|
44
|
+
async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix) {
|
|
45
|
+
const reports = path.join(root, '.sneakoscope', 'reports');
|
|
46
|
+
if (id === 'image_generation') {
|
|
47
|
+
const verified = imageCapability?.codex_app?.available === true || imageCapability?.auth_readiness?.headless_auto_available === true;
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
before: verified ? 'verified' : 'blocked',
|
|
51
|
+
repairability: verified ? 'auto' : 'manual-required',
|
|
52
|
+
repair_actions: verified ? ['postcheck-imagegen-path-contract'] : ['Sign in to Codex App and enable/use the built-in $imagegen surface, or provide explicit imagegen auth for a non-Codex fallback.'],
|
|
53
|
+
after: null,
|
|
54
|
+
artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
|
|
55
|
+
blockers: verified ? [] : ['imagegen_auth_or_codex_app_builtin_missing'],
|
|
56
|
+
warnings: verified ? [] : ['image_generation_not_verified_without_real_capability']
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (id === 'image_followup_edit') {
|
|
60
|
+
const contractReady = await fileExists(path.join(root, '.sneakoscope', 'reports', 'saved-artifact-path-contract.json'));
|
|
61
|
+
return autoState(id, contractReady, path.join(reports, 'saved-artifact-path-contract.json'), ['create-saved-artifact-path-contract']);
|
|
62
|
+
}
|
|
63
|
+
if (id === 'codex_app_screenshot') {
|
|
64
|
+
const dir = path.join(root, '.sneakoscope', 'app-screenshots');
|
|
65
|
+
const ready = await dirWritable(dir);
|
|
66
|
+
return autoState(id, ready, path.join(root, '.sneakoscope', 'app-screenshots', 'screenshot-registry.json'), ['create-app-screenshot-directory', 'create-screenshot-registry']);
|
|
67
|
+
}
|
|
68
|
+
if (id === 'saved_artifact_path_contract') {
|
|
69
|
+
const ready = await fileExists(path.join(reports, 'saved-artifact-path-contract.json'));
|
|
70
|
+
return autoState(id, ready, path.join(reports, 'saved-artifact-path-contract.json'), ['create-saved-artifact-path-contract']);
|
|
71
|
+
}
|
|
72
|
+
if (id === 'app_handoff') {
|
|
73
|
+
const ok = featureOk(nativeFeatureMatrix, 'app_handoff');
|
|
74
|
+
return {
|
|
75
|
+
id,
|
|
76
|
+
before: ok ? 'verified' : 'unknown',
|
|
77
|
+
repairability: ok ? 'auto' : 'manual-required',
|
|
78
|
+
repair_actions: ok ? ['postcheck-app-handoff'] : ['Open Codex App and approve/enable app handoff; rerun `sks doctor --fix --repair-native-capabilities --yes`.'],
|
|
79
|
+
after: null,
|
|
80
|
+
artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
|
|
81
|
+
blockers: ok ? [] : ['codex_app_handoff_not_verified'],
|
|
82
|
+
warnings: ok ? [] : ['manual_app_handoff_approval_required']
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (id === 'image_path_exposure') {
|
|
86
|
+
const ok = featureOk(nativeFeatureMatrix, 'image_path_exposure');
|
|
87
|
+
const fallback = await fileExists(path.join(reports, 'saved-artifact-path-contract.json'));
|
|
88
|
+
return {
|
|
89
|
+
id,
|
|
90
|
+
before: ok ? 'verified' : fallback ? 'degraded' : 'missing',
|
|
91
|
+
repairability: ok ? 'auto' : 'doctor-fix',
|
|
92
|
+
repair_actions: ok ? ['postcheck-image-path-exposure'] : ['create-saved-artifact-path-contract'],
|
|
93
|
+
after: null,
|
|
94
|
+
artifact_path: path.join(reports, 'saved-artifact-path-contract.json'),
|
|
95
|
+
blockers: ok || fallback ? [] : ['image_path_exposure_missing_without_fallback_contract'],
|
|
96
|
+
warnings: ok ? [] : ['using_saved_artifact_path_contract_fallback']
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (id === 'computer_use') {
|
|
100
|
+
const envVerified = process.env.SKS_COMPUTER_USE_CAPABILITY === 'verified';
|
|
101
|
+
return {
|
|
102
|
+
id,
|
|
103
|
+
before: envVerified ? 'verified' : 'unknown',
|
|
104
|
+
repairability: envVerified ? 'auto' : 'manual-required',
|
|
105
|
+
repair_actions: envVerified ? ['postcheck-computer-use'] : ['Enable Codex Computer Use and macOS Screen Recording/Accessibility permissions, then rerun doctor.'],
|
|
106
|
+
after: null,
|
|
107
|
+
artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
|
|
108
|
+
blockers: envVerified ? [] : ['computer_use_os_permission_or_capability_unknown'],
|
|
109
|
+
warnings: envVerified ? [] : ['manual_os_permission_required']
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const chromeReady = process.env.SKS_CHROME_EXTENSION_READY === '1' || featureOk(nativeFeatureMatrix, 'plugin_json');
|
|
113
|
+
return {
|
|
114
|
+
id,
|
|
115
|
+
before: chromeReady ? 'verified' : 'unknown',
|
|
116
|
+
repairability: chromeReady ? 'auto' : 'manual-required',
|
|
117
|
+
repair_actions: chromeReady ? ['postcheck-chrome-extension-readiness'] : ['Install/enable the official Codex Chrome Extension, approve it in Codex App, then rerun QA/browser verification.'],
|
|
118
|
+
after: null,
|
|
119
|
+
artifact_path: path.join(reports, 'native-capability-repair-matrix.json'),
|
|
120
|
+
blockers: chromeReady ? [] : ['codex_chrome_extension_readiness_not_verified'],
|
|
121
|
+
warnings: chromeReady ? [] : ['manual_chrome_extension_setup_required']
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export async function writeSavedArtifactPathContract(root) {
|
|
125
|
+
const artifactPath = path.join(root, '.sneakoscope', 'reports', 'saved-artifact-path-contract.json');
|
|
126
|
+
await writeJsonAtomic(artifactPath, {
|
|
127
|
+
schema: 'sks.saved-artifact-path-contract.v1',
|
|
128
|
+
generated_at: nowIso(),
|
|
129
|
+
root,
|
|
130
|
+
image_artifacts: path.join(root, '.sneakoscope', 'image-artifacts'),
|
|
131
|
+
app_screenshots: path.join(root, '.sneakoscope', 'app-screenshots'),
|
|
132
|
+
model_visible_path_strategy: 'saved-artifact-path',
|
|
133
|
+
raw_secret_values_recorded: false
|
|
134
|
+
});
|
|
135
|
+
return artifactPath;
|
|
136
|
+
}
|
|
137
|
+
export async function ensureNativeCapabilityArtifactRoots(root) {
|
|
138
|
+
const imageDir = path.join(root, '.sneakoscope', 'image-artifacts');
|
|
139
|
+
const appDir = path.join(root, '.sneakoscope', 'app-screenshots');
|
|
140
|
+
await ensureDir(imageDir);
|
|
141
|
+
await ensureDir(appDir);
|
|
142
|
+
const imageRegistry = path.join(imageDir, 'image-artifact-registry.json');
|
|
143
|
+
const screenshotRegistry = path.join(appDir, 'screenshot-registry.json');
|
|
144
|
+
const created = [];
|
|
145
|
+
if (!(await fileExists(imageRegistry))) {
|
|
146
|
+
await writeJsonAtomic(imageRegistry, { schema: 'sks.image-artifact-registry.v1', generated_at: nowIso(), images: [] });
|
|
147
|
+
created.push(imageRegistry);
|
|
148
|
+
}
|
|
149
|
+
if (!(await fileExists(screenshotRegistry))) {
|
|
150
|
+
await writeJsonAtomic(screenshotRegistry, { schema: 'sks.app-screenshot-registry.v1', generated_at: nowIso(), screenshots: [] });
|
|
151
|
+
created.push(screenshotRegistry);
|
|
152
|
+
}
|
|
153
|
+
created.push(await writeSavedArtifactPathContract(root));
|
|
154
|
+
return created;
|
|
155
|
+
}
|
|
156
|
+
function autoState(id, ready, artifactPath, actions) {
|
|
157
|
+
return {
|
|
158
|
+
id,
|
|
159
|
+
before: ready ? 'verified' : 'missing',
|
|
160
|
+
repairability: ready ? 'auto' : 'doctor-fix',
|
|
161
|
+
repair_actions: ready ? [`postcheck-${id}`] : actions,
|
|
162
|
+
after: null,
|
|
163
|
+
artifact_path: artifactPath,
|
|
164
|
+
blockers: ready ? [] : [`${id}_repair_required`],
|
|
165
|
+
warnings: []
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function featureOk(matrix, feature) {
|
|
169
|
+
return matrix?.features?.[feature]?.ok === true;
|
|
170
|
+
}
|
|
171
|
+
async function fileExists(file) {
|
|
172
|
+
try {
|
|
173
|
+
const stat = await fs.stat(file);
|
|
174
|
+
return stat.isFile();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function dirWritable(dir) {
|
|
181
|
+
try {
|
|
182
|
+
await ensureDir(dir);
|
|
183
|
+
await fs.access(dir);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function fixtureImageCapability(mode) {
|
|
191
|
+
return mode === 'all-repairable'
|
|
192
|
+
? { codex_app: { available: true }, auth_readiness: { headless_auto_available: true }, blockers: [] }
|
|
193
|
+
: { codex_app: { available: false }, auth_readiness: { headless_auto_available: false }, blockers: ['fixture_manual_required'] };
|
|
194
|
+
}
|
|
195
|
+
function fixtureNativeFeatureMatrix(mode) {
|
|
196
|
+
const ok = mode === 'all-repairable';
|
|
197
|
+
return {
|
|
198
|
+
ok,
|
|
199
|
+
features: {
|
|
200
|
+
app_handoff: { ok },
|
|
201
|
+
image_path_exposure: { ok },
|
|
202
|
+
plugin_json: { ok }
|
|
203
|
+
},
|
|
204
|
+
blockers: ok ? [] : ['fixture_manual_required']
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function messageOf(err) {
|
|
208
|
+
return err instanceof Error ? err.message : String(err);
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=native-capability-repair-matrix.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { syncCoreSkillsIntegrity } from './core-skill-integrity.js';
|
|
4
|
+
import { buildNativeCapabilityRepairMatrix, ensureNativeCapabilityArtifactRoots } from './native-capability-repair-matrix.js';
|
|
5
|
+
import { postcheckNativeCapabilities } from './native-capability-postcheck.js';
|
|
6
|
+
export async function repairNativeCapabilities(input) {
|
|
7
|
+
const root = path.resolve(input.root);
|
|
8
|
+
const capabilitySelection = input.capabilities ? { capabilities: input.capabilities } : {};
|
|
9
|
+
const before = await buildNativeCapabilityRepairMatrix({
|
|
10
|
+
root,
|
|
11
|
+
...capabilitySelection,
|
|
12
|
+
fixture: input.fixture || false,
|
|
13
|
+
reportPath: path.join(root, '.sneakoscope', 'reports', 'native-capability-repair-matrix-before.json')
|
|
14
|
+
});
|
|
15
|
+
const repaired = [];
|
|
16
|
+
if (input.fix) {
|
|
17
|
+
repaired.push(...await ensureNativeCapabilityArtifactRoots(root));
|
|
18
|
+
await syncCoreSkillsIntegrity({ root, apply: true }).catch(() => undefined);
|
|
19
|
+
}
|
|
20
|
+
const afterMatrix = await buildNativeCapabilityRepairMatrix({
|
|
21
|
+
root,
|
|
22
|
+
...capabilitySelection,
|
|
23
|
+
fixture: input.fixture || false,
|
|
24
|
+
reportPath: path.join(root, '.sneakoscope', 'reports', 'native-capability-repair-matrix.json')
|
|
25
|
+
});
|
|
26
|
+
const postcheck = await postcheckNativeCapabilities({
|
|
27
|
+
root,
|
|
28
|
+
matrix: afterMatrix,
|
|
29
|
+
fixture: input.fixture || false,
|
|
30
|
+
reportPath: path.join(root, '.sneakoscope', 'reports', 'native-capability-postcheck.json')
|
|
31
|
+
});
|
|
32
|
+
await writeJsonAtomic(path.join(root, '.sneakoscope', 'reports', 'native-capability-repair.json'), {
|
|
33
|
+
schema: 'sks.native-capability-repair.v1',
|
|
34
|
+
generated_at: nowIso(),
|
|
35
|
+
ok: postcheck.ok,
|
|
36
|
+
fix: input.fix,
|
|
37
|
+
yes: input.yes,
|
|
38
|
+
before,
|
|
39
|
+
after: postcheck,
|
|
40
|
+
repaired_artifacts: repaired,
|
|
41
|
+
manual_required: postcheck.capabilities
|
|
42
|
+
.filter((state) => state.repairability === 'manual-required' && state.after !== 'verified')
|
|
43
|
+
.map((state) => ({ id: state.id, actions: state.repair_actions }))
|
|
44
|
+
}).catch(() => undefined);
|
|
45
|
+
return postcheck;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=native-capability-repair.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ensureDir, nowIso, writeJsonAtomic } from '../fsx.js';
|
|
4
|
+
import { buildSkillRegistryLedger, groupByCanonical } from './skill-registry-ledger.js';
|
|
5
|
+
export async function dedupeProjectSkills(input) {
|
|
6
|
+
const root = path.resolve(input.root);
|
|
7
|
+
const fix = input.fix === true;
|
|
8
|
+
const yes = input.yes === true;
|
|
9
|
+
const ledger = await buildSkillRegistryLedger({ root });
|
|
10
|
+
const grouped = groupByCanonical(ledger.entries);
|
|
11
|
+
const actions = [];
|
|
12
|
+
const unresolvedUserDuplicates = [];
|
|
13
|
+
for (const [canonical, group] of grouped.entries()) {
|
|
14
|
+
if (group.length <= 1)
|
|
15
|
+
continue;
|
|
16
|
+
group.sort(compareSkillPriority);
|
|
17
|
+
const userEntries = group.filter((entry) => !entry.managed_by_sks);
|
|
18
|
+
const managedEntries = group.filter((entry) => entry.managed_by_sks);
|
|
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'));
|
|
22
|
+
for (const managed of managedEntries) {
|
|
23
|
+
const quarantine = await maybeQuarantine(root, canonical, managed, fix, 'managed collision with user-authored skill');
|
|
24
|
+
actions.push(actionRow(canonical, quarantine ? 'quarantined' : 'reported', managed, quarantine, 'managed collision with user-authored skill'));
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (managedEntries.length > 1) {
|
|
29
|
+
const current = managedEntries.find((entry) => entry.status === 'managed-current') || managedEntries[0];
|
|
30
|
+
if (current)
|
|
31
|
+
actions.push(actionRow(canonical, 'kept', current, null, 'highest-priority SKS-managed skill kept'));
|
|
32
|
+
for (const duplicate of managedEntries.filter((entry) => entry !== current)) {
|
|
33
|
+
const quarantine = await maybeQuarantine(root, canonical, duplicate, fix, 'duplicate SKS-managed skill');
|
|
34
|
+
actions.push(actionRow(canonical, quarantine ? 'quarantined' : 'reported', duplicate, quarantine, 'duplicate SKS-managed skill'));
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (userEntries.length > 1) {
|
|
39
|
+
const keep = userEntries[0];
|
|
40
|
+
if (keep)
|
|
41
|
+
actions.push(actionRow(canonical, 'kept', keep, null, 'highest-priority user-authored skill kept'));
|
|
42
|
+
const shouldMove = fix && yes && input.quarantineUserDuplicates === true;
|
|
43
|
+
for (const duplicate of userEntries.slice(1)) {
|
|
44
|
+
const quarantine = shouldMove ? await quarantineSkill(root, canonical, duplicate, 'user-authored duplicate skill') : null;
|
|
45
|
+
actions.push(actionRow(canonical, quarantine ? 'quarantined' : 'reported', duplicate, quarantine, 'user-authored duplicate skill requires --quarantine-user-duplicates --yes'));
|
|
46
|
+
}
|
|
47
|
+
if (!shouldMove)
|
|
48
|
+
unresolvedUserDuplicates.push(canonical);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
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}`);
|
|
53
|
+
const report = {
|
|
54
|
+
schema: 'sks.project-skill-dedupe.v1',
|
|
55
|
+
generated_at: nowIso(),
|
|
56
|
+
ok: blockers.length === 0,
|
|
57
|
+
root,
|
|
58
|
+
fix,
|
|
59
|
+
yes,
|
|
60
|
+
actions,
|
|
61
|
+
duplicate_canonical_names: duplicateNames,
|
|
62
|
+
unresolved_user_duplicates: unresolvedUserDuplicates,
|
|
63
|
+
blockers
|
|
64
|
+
};
|
|
65
|
+
const reportPath = input.reportPath === null
|
|
66
|
+
? null
|
|
67
|
+
: input.reportPath || path.join(root, '.sneakoscope', 'reports', 'project-skill-dedupe.json');
|
|
68
|
+
if (reportPath)
|
|
69
|
+
await writeJsonAtomic(reportPath, report).catch(() => undefined);
|
|
70
|
+
return report;
|
|
71
|
+
}
|
|
72
|
+
function compareSkillPriority(a, b) {
|
|
73
|
+
const currentA = a.status === 'managed-current' ? 1 : 0;
|
|
74
|
+
const currentB = b.status === 'managed-current' ? 1 : 0;
|
|
75
|
+
return currentB - currentA || b.active_priority - a.active_priority || a.path.localeCompare(b.path);
|
|
76
|
+
}
|
|
77
|
+
function actionRow(canonicalName, action, entry, quarantinePath, reason) {
|
|
78
|
+
return {
|
|
79
|
+
canonical_name: canonicalName,
|
|
80
|
+
action,
|
|
81
|
+
path: entry.path,
|
|
82
|
+
quarantine_path: quarantinePath,
|
|
83
|
+
reason
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function maybeQuarantine(root, canonical, entry, fix, reason) {
|
|
87
|
+
if (!fix)
|
|
88
|
+
return null;
|
|
89
|
+
return quarantineSkill(root, canonical, entry, reason);
|
|
90
|
+
}
|
|
91
|
+
async function quarantineSkill(root, canonical, entry, reason) {
|
|
92
|
+
const sourceDir = path.dirname(entry.path);
|
|
93
|
+
const stamp = `${Date.now()}-${process.pid}`;
|
|
94
|
+
const target = path.join(root, '.sneakoscope', 'quarantine', 'skills', canonical, stamp, path.basename(sourceDir));
|
|
95
|
+
await ensureDir(path.dirname(target));
|
|
96
|
+
await fs.cp(sourceDir, target, { recursive: true, force: false });
|
|
97
|
+
await fs.rm(sourceDir, { recursive: true, force: true });
|
|
98
|
+
await writeJsonAtomic(path.join(target, 'quarantine-record.json'), {
|
|
99
|
+
schema: 'sks.skill-quarantine-record.v1',
|
|
100
|
+
generated_at: nowIso(),
|
|
101
|
+
source_path: sourceDir,
|
|
102
|
+
quarantine_path: target,
|
|
103
|
+
canonical_name: canonical,
|
|
104
|
+
reason,
|
|
105
|
+
content_sha256: entry.content_sha256
|
|
106
|
+
});
|
|
107
|
+
return target;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=project-skill-dedupe.js.map
|