sneakoscope 0.3.0
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/LICENSE +21 -0
- package/README.md +272 -0
- package/bin/sks.mjs +8 -0
- package/docs/PERFORMANCE.md +39 -0
- package/package.json +46 -0
- package/src/cli/main.mjs +358 -0
- package/src/core/codex-adapter.mjs +49 -0
- package/src/core/db-safety.mjs +347 -0
- package/src/core/decision-contract.mjs +120 -0
- package/src/core/fsx.mjs +328 -0
- package/src/core/hooks-runtime.mjs +110 -0
- package/src/core/hproof.mjs +39 -0
- package/src/core/init.mjs +135 -0
- package/src/core/mission.mjs +56 -0
- package/src/core/no-question-guard.mjs +53 -0
- package/src/core/questions.mjs +99 -0
- package/src/core/retention.mjs +140 -0
- package/src/core/rust-accelerator.mjs +19 -0
- package/src/core/triwiki-attention.mjs +68 -0
package/src/cli/main.mjs
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, tmpdir, packageRoot, dirSize, formatBytes } from '../core/fsx.mjs';
|
|
4
|
+
import { initProject } from '../core/init.mjs';
|
|
5
|
+
import { getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
|
|
6
|
+
import { createMission, loadMission, findLatestMission, setCurrent, stateFile } from '../core/mission.mjs';
|
|
7
|
+
import { buildQuestionSchema, writeQuestions } from '../core/questions.mjs';
|
|
8
|
+
import { sealContract, validateAnswers } from '../core/decision-contract.mjs';
|
|
9
|
+
import { containsUserQuestion, noQuestionContinuationReason } from '../core/no-question-guard.mjs';
|
|
10
|
+
import { evaluateDoneGate, defaultDoneGate } from '../core/hproof.mjs';
|
|
11
|
+
import { emitHook } from '../core/hooks-runtime.mjs';
|
|
12
|
+
import { storageReport, enforceRetention } from '../core/retention.mjs';
|
|
13
|
+
import { classifySql, classifyCommand, loadDbSafetyPolicy, safeSupabaseMcpConfig, checkSqlFile, checkDbOperation, scanDbSafety } from '../core/db-safety.mjs';
|
|
14
|
+
import { rustInfo } from '../core/rust-accelerator.mjs';
|
|
15
|
+
|
|
16
|
+
const flag = (args, name) => args.includes(name);
|
|
17
|
+
const promptOf = (args) => args.filter((x) => !String(x).startsWith('--')).join(' ').trim();
|
|
18
|
+
|
|
19
|
+
export async function main(args) {
|
|
20
|
+
const [cmd, sub, ...rest] = args;
|
|
21
|
+
const tail = sub === undefined ? [] : [sub, ...rest];
|
|
22
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') return help();
|
|
23
|
+
if (cmd === 'doctor') return doctor(tail);
|
|
24
|
+
if (cmd === 'init') return init(tail);
|
|
25
|
+
if (cmd === 'selftest') return selftest(tail);
|
|
26
|
+
if (cmd === 'ralph') return ralph(sub, rest);
|
|
27
|
+
if (cmd === 'hook') return emitHook(sub);
|
|
28
|
+
if (cmd === 'profile') return profile(sub, rest);
|
|
29
|
+
if (cmd === 'hproof') return hproof(sub, rest);
|
|
30
|
+
if (cmd === 'memory') return memory(sub, rest);
|
|
31
|
+
if (cmd === 'gx') return gx(sub, rest);
|
|
32
|
+
if (cmd === 'team') return team(tail);
|
|
33
|
+
if (cmd === 'db') return db(sub, rest);
|
|
34
|
+
if (cmd === 'gc') return gc(tail);
|
|
35
|
+
if (cmd === 'stats') return stats(tail);
|
|
36
|
+
console.error(`Unknown command: ${cmd}`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function help() {
|
|
41
|
+
console.log(`Sneakoscope Codex\n\nUsage:\n sks doctor [--fix] [--json]\n sks init\n sks selftest [--mock]\n sks ralph prepare "task"\n sks ralph answer <mission-id|latest> <answers.json>\n sks ralph run <mission-id|latest> [--mock] [--max-cycles N]\n sks ralph status <mission-id|latest>\n sks db policy\n sks db scan [--migrations] [--json]\n sks db mcp-config --project-ref <ref>\n sks db check --sql "DROP TABLE users"\n sks db check --command "supabase db reset"\n sks gc [--dry-run] [--json]\n sks stats [--json]\n`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function doctor(args) {
|
|
45
|
+
const root = await projectRoot();
|
|
46
|
+
if (flag(args, '--fix')) await initProject(root, {});
|
|
47
|
+
const codex = await getCodexInfo();
|
|
48
|
+
const rust = await rustInfo();
|
|
49
|
+
const nodeOk = Number(process.versions.node.split('.')[0]) >= 20;
|
|
50
|
+
const storage = await storageReport(root);
|
|
51
|
+
const pkgBytes = await dirSize(packageRoot()).catch(() => 0);
|
|
52
|
+
const dbPolicyExists = await exists(path.join(root, '.sneakoscope', 'db-safety.json'));
|
|
53
|
+
const dbScan = await scanDbSafety(root).catch((err) => ({ ok: false, findings: [{ id: 'db_safety_scan_failed', severity: 'high', reason: err.message }] }));
|
|
54
|
+
const result = {
|
|
55
|
+
node: { ok: nodeOk, version: process.version }, root, codex, rust,
|
|
56
|
+
sneakoscope: { ok: await exists(path.join(root, '.sneakoscope')) },
|
|
57
|
+
db_guard: { ok: dbPolicyExists && dbScan.ok, policy: dbPolicyExists ? await loadDbSafetyPolicy(root) : null, scan: dbScan },
|
|
58
|
+
hooks: { ok: await exists(path.join(root, '.codex', 'hooks.json')) },
|
|
59
|
+
skills: { ok: await exists(path.join(root, '.agents', 'skills')) },
|
|
60
|
+
package: { bytes: pkgBytes, human: formatBytes(pkgBytes) }, storage
|
|
61
|
+
};
|
|
62
|
+
result.ready = nodeOk && Boolean(codex.bin) && result.sneakoscope.ok && result.db_guard.ok;
|
|
63
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
|
|
64
|
+
console.log('Sneakoscope Codex Doctor\n');
|
|
65
|
+
console.log(`Node: ${nodeOk ? 'ok' : 'fail'} ${process.version}`);
|
|
66
|
+
console.log(`Project: ${root}`);
|
|
67
|
+
console.log(`Codex: ${codex.bin ? 'ok' : 'missing'} ${codex.version || ''}`);
|
|
68
|
+
console.log(`Rust acc.: ${rust.available ? rust.version : 'optional-missing'}`);
|
|
69
|
+
console.log(`State: ${result.sneakoscope.ok ? 'ok' : 'missing .sneakoscope'}`);
|
|
70
|
+
console.log(`DB Guard: ${result.db_guard.ok ? 'ok' : 'blocked'} ${dbScan.findings?.length || 0} finding(s)`);
|
|
71
|
+
console.log(`Hooks: ${result.hooks.ok ? 'ok' : 'missing .codex/hooks.json'}`);
|
|
72
|
+
console.log(`Skills: ${result.skills.ok ? 'ok' : 'missing .agents/skills'}`);
|
|
73
|
+
console.log(`Package: ${result.package.human}`);
|
|
74
|
+
console.log(`Storage: ${storage.total_human || '0 B'}`);
|
|
75
|
+
console.log(`Ready: ${result.ready ? 'yes' : 'no'}`);
|
|
76
|
+
if (!codex.bin) console.log('\nCodex CLI missing. Install separately: npm i -g @openai/codex, or set SKS_CODEX_BIN.');
|
|
77
|
+
if (!result.ready && !flag(args, '--fix')) console.log('Run: sks doctor --fix');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function init(args) {
|
|
81
|
+
const root = await projectRoot();
|
|
82
|
+
const res = await initProject(root, { force: flag(args, '--force') });
|
|
83
|
+
console.log(`Initialized Sneakoscope Codex in ${root}`);
|
|
84
|
+
for (const x of res.created) console.log(`- ${x}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function ralph(sub, args) {
|
|
88
|
+
if (sub === 'prepare') return ralphPrepare(args);
|
|
89
|
+
if (sub === 'answer') return ralphAnswer(args);
|
|
90
|
+
if (sub === 'run') return ralphRun(args);
|
|
91
|
+
if (sub === 'status') return ralphStatus(args);
|
|
92
|
+
console.error('Usage: sks ralph <prepare|answer|run|status>');
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function ralphPrepare(args) {
|
|
97
|
+
const root = await projectRoot();
|
|
98
|
+
if (!(await exists(path.join(root, '.sneakoscope')))) await initProject(root, {});
|
|
99
|
+
const prompt = promptOf(args);
|
|
100
|
+
if (!prompt) throw new Error('Missing task prompt.');
|
|
101
|
+
const { id, dir } = await createMission(root, { mode: 'ralph', prompt });
|
|
102
|
+
const schema = buildQuestionSchema(prompt);
|
|
103
|
+
await writeQuestions(dir, schema);
|
|
104
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'ralph.prepare.questions_created', slots: schema.slots.length });
|
|
105
|
+
console.log(`Mission created: ${id}`);
|
|
106
|
+
console.log('Ralph Prepare completed. Ralph run is locked until all required answers are supplied.');
|
|
107
|
+
console.log(`Questions: ${path.relative(root, path.join(dir, 'questions.md'))}`);
|
|
108
|
+
console.log(`Answer schema: ${path.relative(root, path.join(dir, 'required-answers.schema.json'))}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function ralphAnswer(args) {
|
|
112
|
+
const root = await projectRoot();
|
|
113
|
+
const [missionArg, answerFile] = args;
|
|
114
|
+
const id = await resolveMissionId(root, missionArg);
|
|
115
|
+
if (!id || !answerFile) throw new Error('Usage: sks ralph answer <mission-id|latest> <answers.json>');
|
|
116
|
+
const { dir, mission } = await loadMission(root, id);
|
|
117
|
+
const answers = await readJson(path.resolve(answerFile));
|
|
118
|
+
await writeJsonAtomic(path.join(dir, 'answers.json'), answers);
|
|
119
|
+
const result = await sealContract(dir, mission);
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
console.error('Answer validation failed. Ralph run remains locked.');
|
|
122
|
+
console.error(JSON.stringify(result.validation, null, 2));
|
|
123
|
+
process.exitCode = 2;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'ralph.contract.sealed', hash: result.contract.sealed_hash });
|
|
127
|
+
await setCurrent(root, { mission_id: id, mode: 'RALPH', phase: 'DECISION_CONTRACT_SEALED' });
|
|
128
|
+
console.log(`Decision Contract sealed for ${id}`);
|
|
129
|
+
console.log(`Hash: ${result.contract.sealed_hash}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function ralphRun(args) {
|
|
133
|
+
const root = await projectRoot();
|
|
134
|
+
const id = await resolveMissionId(root, args[0]);
|
|
135
|
+
if (!id) throw new Error('Usage: sks ralph run <mission-id|latest> [--mock]');
|
|
136
|
+
const { dir, mission } = await loadMission(root, id);
|
|
137
|
+
const contractPath = path.join(dir, 'decision-contract.json');
|
|
138
|
+
if (!(await exists(contractPath))) throw new Error('Ralph cannot run: decision-contract.json is missing.');
|
|
139
|
+
const contract = await readJson(contractPath);
|
|
140
|
+
const dbScan = await scanDbSafety(root);
|
|
141
|
+
if (!dbScan.ok) {
|
|
142
|
+
console.error('Ralph cannot run: DB Guardian found unsafe Supabase/MCP/database configuration.');
|
|
143
|
+
console.error(JSON.stringify(dbScan.findings, null, 2));
|
|
144
|
+
process.exitCode = 2;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const maxCycles = readMaxCycles(args, 8);
|
|
148
|
+
const mock = flag(args, '--mock');
|
|
149
|
+
await setCurrent(root, { mission_id: id, mode: 'RALPH', phase: 'RALPH_RUNNING_NO_QUESTIONS', questions_allowed: false });
|
|
150
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'ralph.run.started', maxCycles, mock });
|
|
151
|
+
await enforceRetention(root).catch(() => {});
|
|
152
|
+
const gatePath = path.join(dir, 'done-gate.json');
|
|
153
|
+
if (!(await exists(gatePath))) await writeJsonAtomic(gatePath, defaultDoneGate());
|
|
154
|
+
console.log(`Ralph started: ${id}`);
|
|
155
|
+
console.log('No-question lock active. Database destructive operations are blocked by DB Guard.');
|
|
156
|
+
if (mock) return ralphRunMock(root, id, dir);
|
|
157
|
+
const codex = await getCodexInfo();
|
|
158
|
+
if (!codex.bin) {
|
|
159
|
+
console.error('Codex CLI not found. Running mock loop instead.');
|
|
160
|
+
return ralphRunMock(root, id, dir);
|
|
161
|
+
}
|
|
162
|
+
let last = '';
|
|
163
|
+
for (let cycle = 1; cycle <= maxCycles; cycle++) {
|
|
164
|
+
const cycleDir = path.join(dir, 'ralph', `cycle-${cycle}`);
|
|
165
|
+
const outputFile = path.join(cycleDir, 'final.md');
|
|
166
|
+
const prompt = buildRalphPrompt({ id, mission, contract, cycle, previous: last });
|
|
167
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'ralph.cycle.start', cycle });
|
|
168
|
+
const result = await runCodexExec({ root, prompt, outputFile, json: true, profile: 'sks-ralph', logDir: cycleDir });
|
|
169
|
+
await writeJsonAtomic(path.join(cycleDir, 'process.json'), { code: result.code, stdout_tail: result.stdout, stderr_tail: result.stderr, stdout_bytes: result.stdoutBytes, stderr_bytes: result.stderrBytes, truncated: result.truncated, timed_out: result.timedOut });
|
|
170
|
+
last = await safeReadText(outputFile, result.stdout || result.stderr || '');
|
|
171
|
+
if (containsUserQuestion(last)) {
|
|
172
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'ralph.guard.question_blocked', cycle });
|
|
173
|
+
last = `${last}\n\n${noQuestionContinuationReason()}`;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const gate = await evaluateDoneGate(root, id);
|
|
177
|
+
if (gate.passed) {
|
|
178
|
+
await setCurrent(root, { mission_id: id, mode: 'RALPH', phase: 'RALPH_DONE', questions_allowed: true });
|
|
179
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'ralph.done', cycle });
|
|
180
|
+
await enforceRetention(root).catch(() => {});
|
|
181
|
+
console.log(`Ralph done: ${id}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'ralph.cycle.continue', cycle, reasons: gate.reasons });
|
|
185
|
+
await enforceRetention(root).catch(() => {});
|
|
186
|
+
}
|
|
187
|
+
await setCurrent(root, { mission_id: id, mode: 'RALPH', phase: 'RALPH_PAUSED_MAX_CYCLES', questions_allowed: true });
|
|
188
|
+
console.log(`Ralph paused after max cycles: ${id}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildRalphPrompt({ id, mission, contract, cycle, previous }) {
|
|
192
|
+
return `You are running Sneakoscope Codex Ralph mode.\nMISSION: ${id}\nTASK: ${mission.prompt}\nCYCLE: ${cycle}\nNO-QUESTION LOCK: Do not ask the user. Resolve using decision-contract.json.\nDATABASE SAFETY: Destructive database operations are forbidden. Do not run DROP, TRUNCATE, db reset, db push, branch reset/merge/delete, project deletion, RLS disable, or live execute_sql writes. Use read-only/project-scoped Supabase MCP only unless the sealed contract explicitly allows migration files for local or preview branch.\nDECISION CONTRACT:\n${JSON.stringify(contract, null, 2)}\nPERFORMANCE POLICY: keep outputs concise; raw logs stay in files; summarize evidence only.\nLOOP: plan, read before write, implement within contract, run/justify tests, update .sneakoscope/missions/${id}/done-gate.json.\nPrevious cycle tail:\n${String(previous || '').slice(-2500)}\n`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function safeReadText(file, fallback = '') {
|
|
196
|
+
try { return await fsp.readFile(file, 'utf8'); } catch { return fallback; }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function ralphRunMock(root, id, dir) {
|
|
200
|
+
await writeJsonAtomic(path.join(dir, 'done-gate.json'), { passed: true, unsupported_critical_claims: 0, database_safety_violation: false, database_safety_reviewed: true, visual_drift: 'low', wiki_drift: 'low', tests_required: false, test_evidence_present: false, evidence: ['mock Ralph loop completed'], notes: ['mock run'] });
|
|
201
|
+
await evaluateDoneGate(root, id);
|
|
202
|
+
await setCurrent(root, { mission_id: id, mode: 'RALPH', phase: 'RALPH_DONE', questions_allowed: true });
|
|
203
|
+
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'ralph.mock.done' });
|
|
204
|
+
await enforceRetention(root).catch(() => {});
|
|
205
|
+
console.log(`Mock Ralph done: ${id}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function ralphStatus(args) {
|
|
209
|
+
const root = await projectRoot();
|
|
210
|
+
const id = await resolveMissionId(root, args[0]);
|
|
211
|
+
if (!id) throw new Error('Usage: sks ralph status <mission-id|latest>');
|
|
212
|
+
const { dir, mission } = await loadMission(root, id);
|
|
213
|
+
const state = await readJson(stateFile(root), {});
|
|
214
|
+
const contract = await readJson(path.join(dir, 'decision-contract.json'), null);
|
|
215
|
+
const gate = await readJson(path.join(dir, 'done-gate.evaluated.json'), await readJson(path.join(dir, 'done-gate.json'), null));
|
|
216
|
+
console.log(JSON.stringify({ mission, state, contract_sealed: Boolean(contract), done_gate: gate }, null, 2));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
|
|
220
|
+
function readMaxCycles(args, fallback) { const i = args.indexOf('--max-cycles'); return i >= 0 && args[i + 1] ? Number(args[i + 1]) : fallback; }
|
|
221
|
+
|
|
222
|
+
async function selftest() {
|
|
223
|
+
const tmp = tmpdir();
|
|
224
|
+
process.chdir(tmp);
|
|
225
|
+
await initProject(tmp, {});
|
|
226
|
+
const { id, dir, mission } = await createMission(tmp, { mode: 'ralph', prompt: '로그인 세션 만료 UX 개선 supabase db' });
|
|
227
|
+
const schema = buildQuestionSchema(mission.prompt);
|
|
228
|
+
await writeQuestions(dir, schema);
|
|
229
|
+
if (validateAnswers(schema, {}).ok) throw new Error('selftest failed: empty answers valid');
|
|
230
|
+
const answers = {};
|
|
231
|
+
for (const s of schema.slots) answers[s.id] = s.options ? (s.type === 'array' ? [s.options[0]] : s.options[0]) : (s.type.includes('array') ? ['selftest'] : (s.id === 'DB_MAX_BLAST_RADIUS' ? 'no_live_dml' : 'selftest'));
|
|
232
|
+
await writeJsonAtomic(path.join(dir, 'answers.json'), answers);
|
|
233
|
+
const sealed = await sealContract(dir, mission);
|
|
234
|
+
if (!sealed.ok) throw new Error('selftest failed: answers rejected');
|
|
235
|
+
await setCurrent(tmp, { mission_id: id, mode: 'RALPH', phase: 'RALPH_RUNNING_NO_QUESTIONS' });
|
|
236
|
+
if (!containsUserQuestion('확인해 주세요?')) throw new Error('selftest failed: question guard');
|
|
237
|
+
if (classifySql('drop table users;').level !== 'destructive') throw new Error('selftest failed: destructive sql not detected');
|
|
238
|
+
if (classifyCommand('supabase db reset').level !== 'destructive') throw new Error('selftest failed: supabase db reset not detected');
|
|
239
|
+
const dbDecision = await checkDbOperation(tmp, { mission_id: id }, { tool_name: 'mcp__supabase__execute_sql', sql: 'drop table users;' }, { duringRalph: true });
|
|
240
|
+
if (dbDecision.action !== 'block') throw new Error('selftest failed: destructive MCP SQL allowed');
|
|
241
|
+
await writeJsonAtomic(path.join(dir, 'done-gate.json'), { passed: true, unsupported_critical_claims: 0, database_safety_violation: false, database_safety_reviewed: true, visual_drift: 'low', wiki_drift: 'low', tests_required: false });
|
|
242
|
+
const gate = await evaluateDoneGate(tmp, id);
|
|
243
|
+
if (!gate.passed) throw new Error('selftest failed: done gate');
|
|
244
|
+
const gc = await enforceRetention(tmp, { dryRun: true });
|
|
245
|
+
if (!gc.report.exists) throw new Error('selftest failed: storage report');
|
|
246
|
+
console.log('Sneakoscope Codex selftest passed.');
|
|
247
|
+
console.log(`temp: ${tmp}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function profile(sub, args) {
|
|
251
|
+
const root = await projectRoot();
|
|
252
|
+
if (sub === 'show') return console.log(JSON.stringify(await readJson(path.join(root, '.sneakoscope', 'model', 'current.json'), { model: 'gpt-5.5' }), null, 2));
|
|
253
|
+
if (sub === 'set') { await writeJsonAtomic(path.join(root, '.sneakoscope', 'model', 'current.json'), { model: args[0] || 'gpt-5.5', set_at: nowIso() }); return console.log(`Model profile set: ${args[0] || 'gpt-5.5'}`); }
|
|
254
|
+
console.error('Usage: sks profile show|set <model>');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function hproof(sub, args) {
|
|
258
|
+
if (sub !== 'check') return console.error('Usage: sks hproof check [mission-id]');
|
|
259
|
+
const root = await projectRoot();
|
|
260
|
+
const id = await resolveMissionId(root, args[0]);
|
|
261
|
+
if (!id) throw new Error('No mission found.');
|
|
262
|
+
console.log(JSON.stringify(await evaluateDoneGate(root, id), null, 2));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function memory(sub, args) { return gc(args || []); }
|
|
266
|
+
|
|
267
|
+
async function gc(args) {
|
|
268
|
+
const root = await projectRoot();
|
|
269
|
+
const res = await enforceRetention(root, { dryRun: flag(args, '--dry-run') });
|
|
270
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
|
|
271
|
+
console.log(flag(args, '--dry-run') ? 'Sneakoscope Codex GC dry run' : 'Sneakoscope Codex GC completed');
|
|
272
|
+
console.log(`Storage: ${res.report.total_human || '0 B'}`);
|
|
273
|
+
console.log(`Actions: ${res.actions.length}`);
|
|
274
|
+
for (const a of res.actions.slice(0, 20)) console.log(`- ${a.action} ${a.path || a.mission || ''} ${a.bytes ? formatBytes(a.bytes) : ''}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function stats(args) {
|
|
278
|
+
const root = await projectRoot();
|
|
279
|
+
const report = await storageReport(root);
|
|
280
|
+
const pkgBytes = await dirSize(packageRoot()).catch(() => 0);
|
|
281
|
+
const out = { package: { bytes: pkgBytes, human: formatBytes(pkgBytes) }, storage: report };
|
|
282
|
+
if (flag(args, '--json')) return console.log(JSON.stringify(out, null, 2));
|
|
283
|
+
console.log('Sneakoscope Codex Stats');
|
|
284
|
+
console.log(`Package: ${out.package.human}`);
|
|
285
|
+
console.log(`State: ${report.total_human || '0 B'}`);
|
|
286
|
+
for (const [name, sec] of Object.entries(report.sections || {})) console.log(`- ${name}: ${sec.human}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function gx(sub, args) {
|
|
290
|
+
const root = await projectRoot();
|
|
291
|
+
if (sub === 'init') {
|
|
292
|
+
const name = args[0] || 'architecture-atlas';
|
|
293
|
+
const dir = path.join(root, '.sneakoscope', 'gx', 'cartridges', name);
|
|
294
|
+
await writeJsonAtomic(path.join(dir, 'vgraph.json'), { id: name, version: 1, nodes: [], edges: [], invariants: [], tests: [] });
|
|
295
|
+
await writeJsonAtomic(path.join(dir, 'beta.json'), { id: name, version: 1, read_order: ['grid', 'layers', 'nodes', 'edges', 'tests'] });
|
|
296
|
+
await writeTextAtomic(path.join(dir, 'image-prompt.md'), 'Create a clean technical architecture sheet from vgraph.json. Use GPT Image 2 only.');
|
|
297
|
+
console.log(`GX cartridge initialized: ${path.relative(root, dir)}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (['render', 'validate', 'drift'].includes(sub)) return console.log(`GX ${sub}: metadata only; image generation is performed by Codex $imagegen in live mode.`);
|
|
301
|
+
console.error('Usage: sks gx init|render|validate|drift');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function team(args) {
|
|
305
|
+
const prompt = promptOf(args);
|
|
306
|
+
const root = await projectRoot();
|
|
307
|
+
const { id, dir } = await createMission(root, { mode: 'team', prompt });
|
|
308
|
+
const schema = buildQuestionSchema(prompt);
|
|
309
|
+
await writeQuestions(dir, schema);
|
|
310
|
+
console.log(`Team mission created: ${id}`);
|
|
311
|
+
console.log('Team mode also requires mandatory clarification before implementation.');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function db(sub, args) {
|
|
315
|
+
const root = await projectRoot();
|
|
316
|
+
if (sub === 'policy') {
|
|
317
|
+
console.log(JSON.stringify(await loadDbSafetyPolicy(root), null, 2));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (sub === 'scan') {
|
|
321
|
+
const report = await scanDbSafety(root, { includeMigrations: flag(args, '--migrations') });
|
|
322
|
+
console.log(JSON.stringify(report, null, 2));
|
|
323
|
+
process.exitCode = report.ok ? 0 : 2;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (sub === 'mcp-config') {
|
|
327
|
+
const projectIdx = args.indexOf('--project-ref');
|
|
328
|
+
const featuresIdx = args.indexOf('--features');
|
|
329
|
+
const projectRef = projectIdx >= 0 ? args[projectIdx + 1] : '<project_ref>';
|
|
330
|
+
const features = featuresIdx >= 0 ? args[featuresIdx + 1] : 'database,docs';
|
|
331
|
+
console.log(JSON.stringify(safeSupabaseMcpConfig({ projectRef, readOnly: true, features }), null, 2));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (sub === 'classify' || sub === 'check') {
|
|
335
|
+
const sqlIdx = args.indexOf('--sql');
|
|
336
|
+
const commandIdx = args.indexOf('--command');
|
|
337
|
+
const fileIdx = args.indexOf('--file');
|
|
338
|
+
let result;
|
|
339
|
+
if (fileIdx >= 0 && args[fileIdx + 1]) result = await checkSqlFile(path.resolve(args[fileIdx + 1]));
|
|
340
|
+
else if (commandIdx >= 0 && args[commandIdx + 1]) result = classifyCommand(args[commandIdx + 1]);
|
|
341
|
+
else if (sqlIdx >= 0 && args[sqlIdx + 1]) result = classifySql(args[sqlIdx + 1]);
|
|
342
|
+
else if (sub === 'check' && args[0]) result = await checkSqlFile(path.resolve(args[0]));
|
|
343
|
+
else result = classifySql(args.join(' ').trim());
|
|
344
|
+
console.log(JSON.stringify(result, null, 2));
|
|
345
|
+
process.exitCode = ['destructive', 'write', 'possible_db'].includes(result.level) ? 2 : 0;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (sub === 'scan-payload') {
|
|
349
|
+
const raw = await fsp.readFile(0, 'utf8');
|
|
350
|
+
const payload = raw.trim() ? JSON.parse(raw) : {};
|
|
351
|
+
const decision = await checkDbOperation(root, {}, payload, { duringRalph: false });
|
|
352
|
+
console.log(JSON.stringify(decision, null, 2));
|
|
353
|
+
process.exitCode = decision.action === 'block' ? 2 : 0;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
console.error('Usage: sks db policy | db scan [--migrations] | db mcp-config --project-ref <id> | db check --sql "..." | db check --command "..." | db check --file file.sql');
|
|
357
|
+
process.exitCode = 1;
|
|
358
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { exists, packageRoot, runProcess, which } from './fsx.mjs';
|
|
3
|
+
|
|
4
|
+
export async function findCodexBinary() {
|
|
5
|
+
const env = process.env.SKS_CODEX_BIN || process.env.DCODEX_CODEX_BIN || process.env.CODEX_BIN;
|
|
6
|
+
if (env && await exists(env)) return env;
|
|
7
|
+
const global = await which('codex');
|
|
8
|
+
if (global) return global;
|
|
9
|
+
const root = packageRoot();
|
|
10
|
+
const local = path.join(root, 'node_modules', '.bin', process.platform === 'win32' ? 'codex.cmd' : 'codex');
|
|
11
|
+
if (await exists(local)) return local;
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function codexVersion(bin) {
|
|
16
|
+
if (!bin) return null;
|
|
17
|
+
const result = await runProcess(bin, ['--version'], { timeoutMs: 10000, maxOutputBytes: 16 * 1024 });
|
|
18
|
+
const text = `${result.stdout}${result.stderr}`.trim();
|
|
19
|
+
return result.code === 0 ? text : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function getCodexInfo() {
|
|
23
|
+
const bin = await findCodexBinary();
|
|
24
|
+
const version = await codexVersion(bin);
|
|
25
|
+
return { bin, version, available: Boolean(bin) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function runCodexExec({ root, prompt, outputFile, json = true, profile = null, extraArgs = [], onStdout, onStderr, logDir = null, stdoutFile = null, stderrFile = null, maxBufferBytes = 256 * 1024, timeoutMs = null }) {
|
|
29
|
+
const bin = await findCodexBinary();
|
|
30
|
+
if (!bin) {
|
|
31
|
+
return { code: 127, stdout: '', stderr: 'Codex CLI not found. Install @openai/codex or set SKS_CODEX_BIN.' };
|
|
32
|
+
}
|
|
33
|
+
const args = ['exec', '--cd', root];
|
|
34
|
+
if (profile) args.push('--profile', profile);
|
|
35
|
+
if (json) args.push('--json');
|
|
36
|
+
if (outputFile) args.push('--output-last-message', outputFile);
|
|
37
|
+
args.push(...extraArgs);
|
|
38
|
+
args.push(prompt);
|
|
39
|
+
const effectiveTimeoutMs = Number(timeoutMs || process.env.SKS_CODEX_TIMEOUT_MS || process.env.DCODEX_CODEX_TIMEOUT_MS || 30 * 60 * 1000);
|
|
40
|
+
return runProcess(bin, args, {
|
|
41
|
+
cwd: root,
|
|
42
|
+
onStdout,
|
|
43
|
+
onStderr,
|
|
44
|
+
timeoutMs: effectiveTimeoutMs,
|
|
45
|
+
maxOutputBytes: maxBufferBytes,
|
|
46
|
+
stdoutFile: stdoutFile || (logDir ? path.join(logDir, 'codex.stdout.log') : undefined),
|
|
47
|
+
stderrFile: stderrFile || (logDir ? path.join(logDir, 'codex.stderr.log') : undefined)
|
|
48
|
+
});
|
|
49
|
+
}
|