sneakoscope 3.1.6 → 3.1.8
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 +10 -3
- 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/commands/codex-app.js +20 -2
- package/dist/commands/codex-native.js +18 -2
- package/dist/commands/doctor.js +106 -12
- package/dist/core/codex-app/codex-agent-role-sync.js +9 -5
- package/dist/core/codex-app/codex-init-deep.js +6 -2
- package/dist/core/codex-app/codex-skill-sync.js +80 -152
- package/dist/core/codex-control/codex-0138-capability.js +5 -2
- package/dist/core/codex-native/codex-native-feature-broker.js +74 -6
- package/dist/core/codex-native/codex-native-pattern-analysis.js +14 -2
- package/dist/core/codex-native/codex-native-reference-cache.js +98 -0
- package/dist/core/codex-native/codex-native-reference-source.js +50 -11
- package/dist/core/codex-native/codex-native-repair-transaction.js +150 -0
- 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/codex-plugins/codex-plugin-json.js +5 -2
- package/dist/core/commands/basic-cli.js +15 -9
- package/dist/core/commands/mad-sks-command.js +16 -0
- 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/doctor/doctor-native-capability-repair.js +48 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +5 -1
- package/dist/core/loops/loop-planner.js +1 -1
- package/dist/core/loops/loop-worker-prompts.js +2 -0
- package/dist/core/loops/loop-worker-runtime.js +8 -1
- package/dist/core/version.js +1 -1
- package/dist/scripts/codex-native-runtime-e2e-fixture.js +75 -0
- package/dist/scripts/loop-worker-fixture-child.js +2 -1
- package/dist/scripts/sizecheck.js +8 -2
- package/dist/scripts/sks-3-1-5-directive-check-lib.js +1 -1
- package/dist/scripts/sks-3-1-6-directive-check-lib.js +2 -2
- package/dist/scripts/sks-3-1-7-directive-check-lib.js +58 -0
- package/dist/scripts/sks-3-1-8-check-lib.js +30 -0
- package/package.json +39 -2
|
@@ -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
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function canonicalSkillName(name) {
|
|
2
|
+
const normalizedPath = String(name || '').replace(/\\/g, '/').trim();
|
|
3
|
+
const parts = normalizedPath.split('/').filter(Boolean);
|
|
4
|
+
let leaf = parts.length ? String(parts[parts.length - 1]) : normalizedPath;
|
|
5
|
+
if (/^SKILL\.md$/i.test(leaf) && parts.length > 1)
|
|
6
|
+
leaf = String(parts[parts.length - 2]);
|
|
7
|
+
leaf = leaf.replace(/\.md$/i, '');
|
|
8
|
+
return leaf
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/^[$@#]+/, '')
|
|
12
|
+
.replace(/[_\s]+/g, '-')
|
|
13
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
14
|
+
.replace(/-+/g, '-')
|
|
15
|
+
.replace(/^-|-$/g, '');
|
|
16
|
+
}
|
|
17
|
+
export function skillDisplayNameFromMarkdown(text, fallback) {
|
|
18
|
+
const match = String(text || '').match(/^name:\s*([^\n\r]+)/m);
|
|
19
|
+
return String(match?.[1] || fallback || '').trim().replace(/^["']|["']$/g, '');
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=skill-name-canonicalizer.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { nowIso, readText, sha256, writeJsonAtomic } from '../fsx.js';
|
|
5
|
+
import { buildSksCoreSkillManifest, isSksManagedCoreSkillContent } from './core-skill-manifest.js';
|
|
6
|
+
import { canonicalSkillName, skillDisplayNameFromMarkdown } from './skill-name-canonicalizer.js';
|
|
7
|
+
export async function buildSkillRegistryLedger(input) {
|
|
8
|
+
const root = path.resolve(input.root);
|
|
9
|
+
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
10
|
+
const scanRoots = [
|
|
11
|
+
{ root: path.join(root, '.agents', 'skills'), scope: 'project', priority: 100 },
|
|
12
|
+
{ root: path.join(root, '.codex', 'skills'), scope: 'project', priority: 80 },
|
|
13
|
+
{ root: path.join(codexHome, 'skills'), scope: 'codex-home', priority: 60 },
|
|
14
|
+
...(input.extraRoots || [])
|
|
15
|
+
];
|
|
16
|
+
const manifestByName = new Map(buildSksCoreSkillManifest().skills.map((skill) => [skill.canonical_name, skill.content_sha256]));
|
|
17
|
+
const entries = [];
|
|
18
|
+
for (const scanRoot of scanRoots) {
|
|
19
|
+
const rows = await fs.readdir(scanRoot.root, { withFileTypes: true }).catch(() => []);
|
|
20
|
+
for (const row of rows) {
|
|
21
|
+
if (!row.isDirectory())
|
|
22
|
+
continue;
|
|
23
|
+
const skillPath = path.join(scanRoot.root, row.name, 'SKILL.md');
|
|
24
|
+
const text = await readText(skillPath, null);
|
|
25
|
+
if (typeof text !== 'string')
|
|
26
|
+
continue;
|
|
27
|
+
const displayName = skillDisplayNameFromMarkdown(text, row.name);
|
|
28
|
+
const canonical = canonicalSkillName(displayName || row.name);
|
|
29
|
+
const hash = sha256(text);
|
|
30
|
+
const managed = isSksManagedCoreSkillContent(text) || text.includes('BEGIN SKS MANAGED SKILL');
|
|
31
|
+
const expected = manifestByName.get(canonical);
|
|
32
|
+
const status = managed
|
|
33
|
+
? expected && expected === hash ? 'managed-current' : 'managed-drift'
|
|
34
|
+
: 'user-owned';
|
|
35
|
+
entries.push({
|
|
36
|
+
schema: 'sks.skill-registry-entry.v1',
|
|
37
|
+
root: scanRoot.root,
|
|
38
|
+
scope: scanRoot.scope,
|
|
39
|
+
canonical_name: canonical,
|
|
40
|
+
display_name: displayName || row.name,
|
|
41
|
+
path: skillPath,
|
|
42
|
+
managed_by_sks: managed,
|
|
43
|
+
content_sha256: hash,
|
|
44
|
+
active_priority: scanRoot.priority,
|
|
45
|
+
status,
|
|
46
|
+
blockers: []
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const grouped = groupByCanonical(entries);
|
|
51
|
+
const duplicates = [...grouped.entries()].filter(([, group]) => group.length > 1).map(([name]) => name).sort();
|
|
52
|
+
for (const group of grouped.values()) {
|
|
53
|
+
group.sort((a, b) => b.active_priority - a.active_priority || a.path.localeCompare(b.path));
|
|
54
|
+
group.forEach((entry, index) => {
|
|
55
|
+
if (group.length > 1 && index > 0)
|
|
56
|
+
entry.status = entry.status === 'user-owned' ? 'duplicate' : 'duplicate';
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const blockers = duplicates.map((name) => `duplicate_skill_name:${name}`);
|
|
60
|
+
const ledger = {
|
|
61
|
+
schema: 'sks.skill-registry-ledger.v1',
|
|
62
|
+
generated_at: nowIso(),
|
|
63
|
+
ok: blockers.length === 0,
|
|
64
|
+
root,
|
|
65
|
+
entries,
|
|
66
|
+
duplicate_canonical_names: duplicates,
|
|
67
|
+
blockers
|
|
68
|
+
};
|
|
69
|
+
const reportPath = input.reportPath === null
|
|
70
|
+
? null
|
|
71
|
+
: input.reportPath || path.join(root, '.sneakoscope', 'reports', 'skill-registry-ledger.json');
|
|
72
|
+
if (reportPath)
|
|
73
|
+
await writeJsonAtomic(reportPath, ledger).catch(() => undefined);
|
|
74
|
+
return ledger;
|
|
75
|
+
}
|
|
76
|
+
export function groupByCanonical(entries) {
|
|
77
|
+
const grouped = new Map();
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const current = grouped.get(entry.canonical_name) || [];
|
|
80
|
+
current.push(entry);
|
|
81
|
+
grouped.set(entry.canonical_name, current);
|
|
82
|
+
}
|
|
83
|
+
return grouped;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=skill-registry-ledger.js.map
|
|
@@ -33,7 +33,8 @@ export async function buildCodexPluginInventory() {
|
|
|
33
33
|
});
|
|
34
34
|
const blockers = [
|
|
35
35
|
...(capability.supports_plugin_json ? [] : ['codex_0_138_plugin_json_unavailable']),
|
|
36
|
-
...normalizeList(listJson?.blockers)
|
|
36
|
+
...normalizeList(listJson?.blockers),
|
|
37
|
+
...(process.env.SKS_CODEX_PLUGIN_JSON_FAKE_NO_MCP === '1' ? ['fixture_mcp_candidates_disabled'] : [])
|
|
37
38
|
];
|
|
38
39
|
return {
|
|
39
40
|
schema: 'sks.codex-plugin-inventory.v1',
|
|
@@ -169,7 +170,9 @@ function fakePluginDetail(pluginId) {
|
|
|
169
170
|
installed: true,
|
|
170
171
|
enabled: true,
|
|
171
172
|
default_prompts: ['Use the fixture plugin safely.'],
|
|
172
|
-
remote_mcp_servers:
|
|
173
|
+
remote_mcp_servers: process.env.SKS_CODEX_PLUGIN_JSON_FAKE_NO_MCP === '1'
|
|
174
|
+
? []
|
|
175
|
+
: [{ name: 'fixture-db-docs', url: 'https://mcp.example.test', auth_type: 'oauth' }],
|
|
173
176
|
unavailable_app_templates: ['fixture-desktop-template']
|
|
174
177
|
};
|
|
175
178
|
}
|
|
@@ -11,6 +11,7 @@ import { hooksExplainReport } from '../../cli/feature-commands.js';
|
|
|
11
11
|
import { writeSelftestRouteProof } from '../proof/selftest-proof-fixtures.js';
|
|
12
12
|
import { createMission } from '../mission.js';
|
|
13
13
|
import { formatSksUpdateCheckText, runSksUpdateCheck, runSksUpdateNow } from '../update-check.js';
|
|
14
|
+
import { withSecretPreservationGuard } from '../config/config-migration-journal.js';
|
|
14
15
|
export async function helpCommand(args = []) {
|
|
15
16
|
const topic = args[0];
|
|
16
17
|
if (topic)
|
|
@@ -118,12 +119,13 @@ export async function updateCommand(sub = 'check', args = []) {
|
|
|
118
119
|
process.exitCode = 1;
|
|
119
120
|
return;
|
|
120
121
|
}
|
|
121
|
-
const
|
|
122
|
+
const root = await projectRoot();
|
|
123
|
+
const result = await withSecretPreservationGuard(root, 'update-now', async () => runSksUpdateNow({
|
|
122
124
|
version: valueAfter(args, '--version') || valueAfter(args, '-v'),
|
|
123
125
|
dryRun: flag(args, '--dry-run'),
|
|
124
126
|
timeoutMs: 10 * 60 * 1000,
|
|
125
127
|
maxOutputBytes: 128 * 1024
|
|
126
|
-
});
|
|
128
|
+
}));
|
|
127
129
|
if (flag(args, '--json'))
|
|
128
130
|
return printJson(result);
|
|
129
131
|
console.log(`${sksTextLogo()}\n`);
|
|
@@ -140,14 +142,18 @@ export async function updateCommand(sub = 'check', args = []) {
|
|
|
140
142
|
export async function setupCommand(args = []) {
|
|
141
143
|
const root = await projectRoot();
|
|
142
144
|
const installScope = installScopeFromArgs(args);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
let res = null;
|
|
146
|
+
let cliTools = null;
|
|
147
|
+
await withSecretPreservationGuard(root, 'setup-command', async () => {
|
|
148
|
+
res = await initProject(root, {
|
|
149
|
+
force: flag(args, '--force'),
|
|
150
|
+
installScope,
|
|
151
|
+
localOnly: flag(args, '--local-only'),
|
|
152
|
+
globalCommand: 'sks'
|
|
153
|
+
});
|
|
154
|
+
const { ensureRelatedCliTools } = await import('../../cli/install-helpers.js');
|
|
155
|
+
cliTools = await ensureRelatedCliTools(args);
|
|
148
156
|
});
|
|
149
|
-
const { ensureRelatedCliTools } = await import('../../cli/install-helpers.js');
|
|
150
|
-
const cliTools = await ensureRelatedCliTools(args);
|
|
151
157
|
const result = {
|
|
152
158
|
schema: 'sks.setup.v1',
|
|
153
159
|
ok: true,
|
|
@@ -22,6 +22,7 @@ import { checkSksUpdateNotice } from '../update/update-notice.js';
|
|
|
22
22
|
import { createMadDbCapability, MAD_DB_ACK } from '../mad-db/mad-db-capability.js';
|
|
23
23
|
import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
|
|
24
24
|
import { writeCodex0139CapabilityArtifacts } from '../codex-control/codex-0139-capability.js';
|
|
25
|
+
import { resolveCodexNativeInvocationPlan } from '../codex-native/codex-native-invocation-router.js';
|
|
25
26
|
import { repairZellijForSks } from '../zellij/zellij-self-heal.js';
|
|
26
27
|
export async function madHighCommand(args = [], deps = {}) {
|
|
27
28
|
const subcommand = firstSubcommand(args);
|
|
@@ -478,6 +479,14 @@ async function activateMadZellijPermissionState(cwd = process.cwd(), args = [])
|
|
|
478
479
|
const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: 'sks --mad Zellij scoped high-power maintenance session' });
|
|
479
480
|
await writeCodex0138CapabilityArtifacts(root, { missionId: id }).catch(() => null);
|
|
480
481
|
await writeCodex0139CapabilityArtifacts(root, { missionId: id }).catch(() => null);
|
|
482
|
+
const codexNativeInvocation = await resolveCodexNativeInvocationPlan({
|
|
483
|
+
root,
|
|
484
|
+
missionId: id,
|
|
485
|
+
route: '$MAD',
|
|
486
|
+
desiredCapability: 'hook-evidence'
|
|
487
|
+
}).catch(() => null);
|
|
488
|
+
if (codexNativeInvocation)
|
|
489
|
+
await writeJsonAtomic(path.join(dir, 'mad-codex-native-invocation.json'), codexNativeInvocation).catch(() => undefined);
|
|
481
490
|
const protectedCore = resolveProtectedCore({ packageRoot: packageRoot(), targetRoot: cwd });
|
|
482
491
|
// The interactive launch 'before' snapshot is only persisted (env + policy json)
|
|
483
492
|
// and is never compared against an 'after' snapshot during the session, so the
|
|
@@ -521,6 +530,13 @@ async function activateMadZellijPermissionState(cwd = process.cwd(), args = [])
|
|
|
521
530
|
protected_core_policy: protectedCorePolicyPath,
|
|
522
531
|
protected_core_before: protectedCoreBeforePath,
|
|
523
532
|
protected_core_digest: protectedCoreBefore.digest,
|
|
533
|
+
codex_native_invocation_plan: codexNativeInvocation ? {
|
|
534
|
+
selected_strategy: codexNativeInvocation.selected_strategy,
|
|
535
|
+
hook_evidence_policy: codexNativeInvocation.env.SKS_CODEX_NATIVE_HOOK_EVIDENCE_POLICY,
|
|
536
|
+
blockers: codexNativeInvocation.blockers,
|
|
537
|
+
warnings: codexNativeInvocation.warnings,
|
|
538
|
+
artifact_path: 'mad-codex-native-invocation.json'
|
|
539
|
+
} : null,
|
|
524
540
|
activated_by: 'sks --mad',
|
|
525
541
|
cwd: path.resolve(cwd || process.cwd())
|
|
526
542
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { captureSecretPreservationSnapshot, withSecretPreservationGuard } from './secret-preservation.js';
|
|
4
|
+
export { withSecretPreservationGuard };
|
|
5
|
+
export async function writeSecretMigrationJournal(root, operationName, filesTouched = []) {
|
|
6
|
+
const resolvedRoot = path.resolve(root);
|
|
7
|
+
const snapshot = await captureSecretPreservationSnapshot({ root: resolvedRoot });
|
|
8
|
+
const journalPath = path.join(resolvedRoot, '.sneakoscope', 'reports', 'secret-migration-journal.json');
|
|
9
|
+
const current = await readJson(journalPath, { entries: [] }).catch(() => ({ entries: [] }));
|
|
10
|
+
const entries = Array.isArray(current.entries) ? current.entries : [];
|
|
11
|
+
const entry = {
|
|
12
|
+
schema: 'sks.secret-migration-journal-entry.v1',
|
|
13
|
+
generated_at: nowIso(),
|
|
14
|
+
operation: operationName,
|
|
15
|
+
files_touched: filesTouched,
|
|
16
|
+
protected_keys_present: snapshot.fingerprints.filter((fp) => fp.present).map((fp) => ({ key: fp.key, source: fp.source })),
|
|
17
|
+
raw_values_recorded: false
|
|
18
|
+
};
|
|
19
|
+
const journal = {
|
|
20
|
+
schema: 'sks.secret-migration-journal.v1',
|
|
21
|
+
generated_at: nowIso(),
|
|
22
|
+
entries: [...entries, entry]
|
|
23
|
+
};
|
|
24
|
+
await writeJsonAtomic(journalPath, journal);
|
|
25
|
+
return { journal_path: journalPath, entry };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=config-migration-journal.js.map
|