sneakoscope 0.3.0 → 0.4.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/README.md CHANGED
@@ -1,6 +1,10 @@
1
- # Sneakoscope Codex
1
+ <p align="center">
2
+ <img src="docs/assets/sneakoscope-codex-logo.svg" alt="Sneakoscope Codex logo" width="180">
3
+ </p>
2
4
 
3
- Sneakoscope Codex is a zero-runtime-dependency Node.js harness for running Codex CLI in a more controlled project workflow. It adds mandatory clarification before autonomous work, a Ralph no-question execution loop, H-Proof completion gates, conservative database safety checks, bounded logs/storage, and optional GPT Image 2 visual cartridges.
5
+ <h1 align="center">Sneakoscope Codex</h1>
6
+
7
+ Sneakoscope Codex is a zero-runtime-dependency Node.js harness for running Codex CLI in a more controlled project workflow. It adds mandatory clarification before autonomous work, a Ralph no-question execution loop, H-Proof completion gates, conservative database safety checks, bounded logs/storage, and deterministic GX visual context cartridges.
4
8
 
5
9
  ```bash
6
10
  npm i -g sneakoscope
@@ -55,7 +59,7 @@ sks ralph run latest --mock
55
59
  - **Database guard**: destructive DB operations, production writes, unsafe Supabase MCP configuration, and direct live SQL mutations are blocked or warned on.
56
60
  - **H-Proof done gate**: completion requires supported critical claims, reviewed DB safety state, acceptable visual/wiki drift, and required test evidence.
57
61
  - **Bounded runtime state**: child process output is tailed, logs are rotated/compacted, and old mission artifacts can be pruned.
58
- - **Visual cartridges**: `gx` creates metadata-first visual cartridges where `vgraph.json` remains the source of truth and image generation is delegated to Codex/GPT Image 2.
62
+ - **Visual cartridges**: `gx` creates deterministic SVG/HTML visual context from `vgraph.json` and `beta.json`; no generated-image service is required.
59
63
 
60
64
  ## Ralph Workflow
61
65
 
@@ -82,7 +86,7 @@ Core invariants:
82
86
  3. New ambiguity during `run` is resolved by the sealed decision ladder.
83
87
  4. Hooks help enforce the policy, but the Sneakoscope Codex supervisor and mission files remain the source of truth.
84
88
  5. Database destructive operations are never allowed.
85
- 6. Generated images are not authoritative; `vgraph.json` is.
89
+ 6. Rendered GX files are reproducible context artifacts; `vgraph.json` is authoritative.
86
90
  7. Unsupported critical claims block completion.
87
91
 
88
92
  ## Commands
@@ -108,7 +112,10 @@ sks db check --file ./migration.sql
108
112
 
109
113
  sks hproof check [mission-id|latest]
110
114
  sks gx init [name]
111
- sks gx render|validate|drift
115
+ sks gx render [name] [--format svg|html|all]
116
+ sks gx validate [name]
117
+ sks gx drift [name]
118
+ sks gx snapshot [name]
112
119
  sks profile show
113
120
  sks profile set <model>
114
121
  sks gc [--dry-run] [--json]
@@ -217,20 +224,26 @@ This creates:
217
224
  ```text
218
225
  .sneakoscope/gx/cartridges/<name>/vgraph.json
219
226
  .sneakoscope/gx/cartridges/<name>/beta.json
220
- .sneakoscope/gx/cartridges/<name>/image-prompt.md
227
+ .sneakoscope/gx/cartridges/<name>/render.svg
228
+ .sneakoscope/gx/cartridges/<name>/render.html
229
+ .sneakoscope/gx/cartridges/<name>/validation.json
230
+ .sneakoscope/gx/cartridges/<name>/drift.json
221
231
  ```
222
232
 
223
- The intended flow is metadata first:
233
+ The intended flow is source first and deterministic:
224
234
 
225
235
  ```text
226
236
  vgraph.json
227
- -> image-prompt.md
228
- -> Codex $imagegen / GPT Image 2
229
- -> sheet.png
230
- -> vision parse.json
231
- -> validate against vgraph.json
237
+ + beta.json
238
+ -> sks gx render
239
+ -> render.svg / render.html
240
+ -> sks gx validate
241
+ -> sks gx drift
242
+ -> sks gx snapshot
232
243
  ```
233
244
 
245
+ `render.svg` embeds the normalized `vgraph.json` hash. `sks gx drift` fails when the render is missing, stale, or structurally invalid.
246
+
234
247
  ## TriWiki Context Compression
235
248
 
236
249
  TriWiki is a harness-level context selection strategy, not a model-internal modification. It scores claims and memory entries by geometric distance, authority, freshness, risk, and token cost, then builds small context capsules for the current mission.
@@ -251,6 +264,7 @@ Q0 raw logs only when necessary
251
264
  bin/sks.mjs CLI executable
252
265
  src/cli/main.mjs command router and Ralph loop
253
266
  src/core/db-safety.mjs SQL, CLI, and MCP payload classifier
267
+ src/core/gx-renderer.mjs deterministic SVG/HTML visual context renderer
254
268
  src/core/hproof.mjs done-gate evaluator
255
269
  src/core/init.mjs project bootstrap and hook/skill installation
256
270
  src/core/retention.mjs storage report and garbage collection policy
@@ -266,7 +280,10 @@ The published npm package is allowlisted to `bin`, `src`, `docs`, `README.md`, a
266
280
  ```bash
267
281
  npm run packcheck
268
282
  npm run selftest
283
+ npm run sizecheck
269
284
  npm run doctor
270
285
  ```
271
286
 
287
+ `npm run sizecheck` blocks accidental package bloat before `npm pack` or `npm publish`. Defaults: packed tarball `<=96 KiB`, unpacked package `<=320 KiB`, package files `<=40`, and each tracked file `<=256 KiB`. Override only for an intentional release with `SKS_MAX_PACK_BYTES`, `SKS_MAX_UNPACKED_BYTES`, `SKS_MAX_PACK_FILES`, or `SKS_MAX_TRACKED_FILE_BYTES`.
288
+
272
289
  `npm run selftest` uses the mock path and does not call a model. Live Ralph runs require a working Codex CLI installation and authentication.
@@ -1,12 +1,13 @@
1
1
  # Sneakoscope Codex performance and leak policy
2
2
 
3
- Sneakoscope Codex v0.2 is designed to keep runtime, package size, RAM, and storage bounded.
3
+ Sneakoscope Codex v0.4 is designed to keep runtime, package size, RAM, and storage bounded.
4
4
 
5
5
  ## Speed
6
6
 
7
7
  - `codex exec` output is streamed to files and only a bounded tail is retained in memory.
8
8
  - Ralph cycles run under a timeout and bounded max cycles.
9
9
  - TriWiki claim selection uses bounded top-K selection instead of sorting unbounded context into prompts.
10
+ - GX visual context renders deterministic SVG/HTML from JSON sources, avoiding external image-generation latency, cost, and nondeterminism.
10
11
  - `sks gc` runs after Ralph cycles by default.
11
12
 
12
13
  ## Package size
@@ -14,6 +15,8 @@ Sneakoscope Codex v0.2 is designed to keep runtime, package size, RAM, and stora
14
15
  - The npm package has zero runtime dependencies.
15
16
  - `@openai/codex` is no longer bundled. Users install Codex separately or set `SKS_CODEX_BIN`.
16
17
  - Optional Rust source is in `crates/` for the Git repo, but is excluded from the npm package by the `files` allowlist.
18
+ - GX rendering uses only built-in Node.js APIs and ships as source in the npm package.
19
+ - `npm run sizecheck` enforces package limits before pack/publish: `<=96 KiB` packed, `<=320 KiB` unpacked, `<=40` package files, and `<=256 KiB` per tracked file by default.
17
20
 
18
21
  ## Memory leaks
19
22
 
@@ -37,3 +40,9 @@ Rust is useful for CPU-heavy long-running kernels, but not for the default npm p
37
40
  Sneakoscope Codex v0.3 adds a DB Safety Guard without adding runtime dependencies. It scans hook payloads and CLI commands with bounded string traversal and blocks high-risk database operations before Codex can execute them.
38
41
 
39
42
  Blocked classes include destructive SQL, direct remote SQL mutation, `supabase db reset`, `supabase db push`, migration history repair/squash, and project/branch destructive commands. The guard is intentionally conservative: when unsure, it blocks or warns rather than allowing a potentially destructive database operation.
43
+
44
+ ## GX visual context policy
45
+
46
+ Sneakoscope Codex v0.4 replaces model-rendered visual cartridges with deterministic code-rendered context sheets. `vgraph.json` and `beta.json` are the inputs, `render.svg` and `render.html` are reproducible outputs, and `drift.json` records whether the rendered source hash still matches the current graph.
47
+
48
+ This keeps visual context cheap to regenerate, diffable in normal tooling, and safe to validate during npm packaging without network calls or model access.
@@ -0,0 +1,51 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title desc">
2
+ <title id="title">Sneakoscope Codex logo</title>
3
+ <desc id="desc">A brass magical danger-sensing lens with a red core, orbiting rings, and SKS initials.</desc>
4
+ <defs>
5
+ <radialGradient id="lens" cx="50%" cy="42%" r="58%">
6
+ <stop offset="0%" stop-color="#fff4c2"/>
7
+ <stop offset="42%" stop-color="#f0b54f"/>
8
+ <stop offset="100%" stop-color="#7b241f"/>
9
+ </radialGradient>
10
+ <linearGradient id="brass" x1="92" x2="420" y1="80" y2="432">
11
+ <stop offset="0%" stop-color="#ffe28a"/>
12
+ <stop offset="46%" stop-color="#c9872e"/>
13
+ <stop offset="100%" stop-color="#6c3218"/>
14
+ </linearGradient>
15
+ <linearGradient id="ink" x1="160" x2="352" y1="154" y2="358">
16
+ <stop offset="0%" stop-color="#311018"/>
17
+ <stop offset="100%" stop-color="#15070b"/>
18
+ </linearGradient>
19
+ <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
20
+ <feDropShadow dx="0" dy="16" stdDeviation="18" flood-color="#12070a" flood-opacity=".28"/>
21
+ </filter>
22
+ </defs>
23
+
24
+ <rect width="512" height="512" rx="108" fill="#16080d"/>
25
+ <path fill="#241016" d="M64 346c61 66 122 96 193 96 72 0 138-34 197-101v106c0 21-17 38-38 38H102c-21 0-38-17-38-38V346Z" opacity=".65"/>
26
+
27
+ <g filter="url(#shadow)">
28
+ <path fill="none" stroke="url(#brass)" stroke-width="16" stroke-linecap="round" d="M99 256c41-82 95-123 158-123 62 0 115 41 156 123-41 82-94 123-156 123-63 0-117-41-158-123Z"/>
29
+ <path fill="none" stroke="#f4c56a" stroke-width="5" stroke-linecap="round" d="M126 256c34-61 78-92 131-92 52 0 96 31 130 92-34 61-78 92-130 92-53 0-97-31-131-92Z" opacity=".86"/>
30
+
31
+ <circle cx="256" cy="256" r="93" fill="url(#brass)"/>
32
+ <circle cx="256" cy="256" r="74" fill="url(#ink)"/>
33
+ <circle cx="256" cy="256" r="56" fill="url(#lens)"/>
34
+ <circle cx="237" cy="236" r="16" fill="#fff8d8" opacity=".82"/>
35
+ <circle cx="283" cy="282" r="9" fill="#5b1017" opacity=".75"/>
36
+
37
+ <path fill="#fff0a4" d="M256 176l14 57 55-18-42 39 43 38-56-16-14 57-14-57-56 16 43-38-42-39 55 18 14-57Z"/>
38
+ <circle cx="256" cy="256" r="17" fill="#7b0f17"/>
39
+
40
+ <path fill="none" stroke="#f6d37c" stroke-width="11" stroke-linecap="round" d="M256 75v36M256 401v36M75 256h36M401 256h36"/>
41
+ <path fill="none" stroke="#b86232" stroke-width="8" stroke-linecap="round" d="M133 132l26 26M353 354l26 26M379 132l-26 26M159 354l-26 26"/>
42
+
43
+ <g fill="#fff0bc">
44
+ <circle cx="127" cy="113" r="7"/>
45
+ <circle cx="385" cy="113" r="7"/>
46
+ <circle cx="127" cy="399" r="7"/>
47
+ <circle cx="385" cy="399" r="7"/>
48
+ </g>
49
+ </g>
50
+
51
+ </svg>
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "Sneakoscope Codex",
4
- "version": "0.3.0",
5
- "description": "Sneakoscope Codex: database-safe, performance-bounded Codex CLI harness with Ralph no-question loop, H-Proof gates, GPT Image 2 workflow, and TriWiki compression.",
4
+ "version": "0.4.0",
5
+ "description": "Sneakoscope Codex: database-safe, performance-bounded Codex CLI harness with Ralph no-question loop, H-Proof gates, deterministic GX visual context, and TriWiki compression.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "sks": "bin/sks.mjs"
@@ -20,18 +20,24 @@
20
20
  "scripts": {
21
21
  "selftest": "node ./bin/sks.mjs selftest --mock",
22
22
  "doctor": "node ./bin/sks.mjs doctor",
23
- "packcheck": "find bin src -name '*.mjs' -print0 | xargs -0 -n1 node --check",
24
- "prepack": "npm run packcheck && npm run selftest",
25
- "prepublishOnly": "npm run packcheck && npm run selftest"
23
+ "packcheck": "find bin src scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check",
24
+ "sizecheck": "node ./scripts/sizecheck.mjs",
25
+ "prepack": "npm run packcheck && npm run selftest && npm run sizecheck",
26
+ "prepublishOnly": "npm run packcheck && npm run selftest && npm run sizecheck"
26
27
  },
27
28
  "keywords": [
29
+ "sneakoscope",
28
30
  "codex",
29
31
  "sks",
32
+ "cli",
30
33
  "ai-agent",
31
34
  "harness",
32
35
  "ralph",
33
36
  "llm-wiki",
34
- "gpt-image-2",
37
+ "gx",
38
+ "svg",
39
+ "deterministic",
40
+ "visual-context",
35
41
  "resource-safe",
36
42
  "database-safe",
37
43
  "supabase-mcp",
package/src/cli/main.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
- import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, tmpdir, packageRoot, dirSize, formatBytes } from '../core/fsx.mjs';
3
+ import { projectRoot, readJson, writeJsonAtomic, appendJsonlBounded, nowIso, exists, tmpdir, packageRoot, dirSize, formatBytes } from '../core/fsx.mjs';
4
4
  import { initProject } from '../core/init.mjs';
5
5
  import { getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
6
6
  import { createMission, loadMission, findLatestMission, setCurrent, stateFile } from '../core/mission.mjs';
@@ -12,6 +12,7 @@ import { emitHook } from '../core/hooks-runtime.mjs';
12
12
  import { storageReport, enforceRetention } from '../core/retention.mjs';
13
13
  import { classifySql, classifyCommand, loadDbSafetyPolicy, safeSupabaseMcpConfig, checkSqlFile, checkDbOperation, scanDbSafety } from '../core/db-safety.mjs';
14
14
  import { rustInfo } from '../core/rust-accelerator.mjs';
15
+ import { renderCartridge, validateCartridge, driftCartridge, snapshotCartridge } from '../core/gx-renderer.mjs';
15
16
 
16
17
  const flag = (args, name) => args.includes(name);
17
18
  const promptOf = (args) => args.filter((x) => !String(x).startsWith('--')).join(' ').trim();
@@ -38,7 +39,29 @@ export async function main(args) {
38
39
  }
39
40
 
40
41
  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
+ console.log(`Sneakoscope Codex
43
+
44
+ Usage:
45
+ sks doctor [--fix] [--json]
46
+ sks init
47
+ sks selftest [--mock]
48
+ sks ralph prepare "task"
49
+ sks ralph answer <mission-id|latest> <answers.json>
50
+ sks ralph run <mission-id|latest> [--mock] [--max-cycles N]
51
+ sks ralph status <mission-id|latest>
52
+ sks db policy
53
+ sks db scan [--migrations] [--json]
54
+ sks db mcp-config --project-ref <ref>
55
+ sks db check --sql "DROP TABLE users"
56
+ sks db check --command "supabase db reset"
57
+ sks gx init [name]
58
+ sks gx render [name] [--format svg|html|all]
59
+ sks gx validate [name]
60
+ sks gx drift [name]
61
+ sks gx snapshot [name]
62
+ sks gc [--dry-run] [--json]
63
+ sks stats [--json]
64
+ `);
42
65
  }
43
66
 
44
67
  async function doctor(args) {
@@ -241,6 +264,17 @@ async function selftest() {
241
264
  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
265
  const gate = await evaluateDoneGate(tmp, id);
243
266
  if (!gate.passed) throw new Error('selftest failed: done gate');
267
+ const gxDir = path.join(tmp, '.sneakoscope', 'gx', 'cartridges', 'selftest');
268
+ await writeJsonAtomic(path.join(gxDir, 'vgraph.json'), defaultVGraph('selftest'));
269
+ await writeJsonAtomic(path.join(gxDir, 'beta.json'), defaultBeta('selftest'));
270
+ const render = await renderCartridge(gxDir, { format: 'all' });
271
+ if (!render.outputs.includes('render.svg')) throw new Error('selftest failed: gx svg not rendered');
272
+ const validation = await validateCartridge(gxDir);
273
+ if (!validation.ok) throw new Error('selftest failed: gx validation rejected');
274
+ const drift = await driftCartridge(gxDir);
275
+ if (drift.status !== 'low') throw new Error('selftest failed: gx drift is high');
276
+ const snapshot = await snapshotCartridge(gxDir);
277
+ if (!snapshot.files.svg || !snapshot.files.html) throw new Error('selftest failed: gx snapshot incomplete');
244
278
  const gc = await enforceRetention(tmp, { dryRun: true });
245
279
  if (!gc.report.exists) throw new Error('selftest failed: storage report');
246
280
  console.log('Sneakoscope Codex selftest passed.');
@@ -286,19 +320,114 @@ async function stats(args) {
286
320
  for (const [name, sec] of Object.entries(report.sections || {})) console.log(`- ${name}: ${sec.human}`);
287
321
  }
288
322
 
323
+ function positionalArgs(args = []) {
324
+ const out = [];
325
+ for (let i = 0; i < args.length; i++) {
326
+ const arg = String(args[i]);
327
+ if (arg === '--format') {
328
+ i++;
329
+ continue;
330
+ }
331
+ if (!arg.startsWith('--')) out.push(arg);
332
+ }
333
+ return out;
334
+ }
335
+
336
+ function readFlagValue(args, name, fallback) {
337
+ const i = args.indexOf(name);
338
+ return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
339
+ }
340
+
341
+ function cartridgeName(args, fallback = 'architecture-atlas') {
342
+ const raw = positionalArgs(args)[0] || fallback;
343
+ return String(raw).trim().replace(/[\\/]+/g, '-').replace(/[^A-Za-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '') || fallback;
344
+ }
345
+
346
+ function cartridgeDir(root, name) {
347
+ return path.join(root, '.sneakoscope', 'gx', 'cartridges', name);
348
+ }
349
+
350
+ function defaultVGraph(name) {
351
+ return {
352
+ id: name,
353
+ title: 'Sneakoscope Context Map',
354
+ version: 1,
355
+ nodes: [
356
+ { id: 'source', label: 'vgraph source', kind: 'source', layer: 'input', status: 'safe' },
357
+ { id: 'contract', label: 'decision contract', kind: 'guard', layer: 'policy', status: 'safe' },
358
+ { id: 'proof', label: 'H-Proof gate', kind: 'guard', layer: 'verification', status: 'safe' }
359
+ ],
360
+ edges: [
361
+ { from: 'source', to: 'contract', label: 'constrains' },
362
+ { from: 'contract', to: 'proof', label: 'verifies' }
363
+ ],
364
+ invariants: [
365
+ 'vgraph.json remains the source of truth',
366
+ 'rendered SVG hash must match source hash'
367
+ ],
368
+ tests: [
369
+ 'sks gx validate',
370
+ 'sks gx drift'
371
+ ],
372
+ risks: []
373
+ };
374
+ }
375
+
376
+ function defaultBeta(name) {
377
+ return {
378
+ id: name,
379
+ version: 1,
380
+ read_order: ['title', 'layers', 'nodes', 'edges', 'invariants', 'tests'],
381
+ renderer: 'sneakoscope-codex-deterministic-svg'
382
+ };
383
+ }
384
+
289
385
  async function gx(sub, args) {
290
386
  const root = await projectRoot();
387
+ const name = cartridgeName(args);
388
+ const dir = cartridgeDir(root, name);
291
389
  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)}`);
390
+ const vgraphPath = path.join(dir, 'vgraph.json');
391
+ const betaPath = path.join(dir, 'beta.json');
392
+ const created = [];
393
+ if (!(await exists(vgraphPath)) || flag(args, '--force')) {
394
+ await writeJsonAtomic(vgraphPath, defaultVGraph(name));
395
+ created.push('vgraph.json');
396
+ }
397
+ if (!(await exists(betaPath)) || flag(args, '--force')) {
398
+ await writeJsonAtomic(betaPath, defaultBeta(name));
399
+ created.push('beta.json');
400
+ }
401
+ const render = await renderCartridge(dir, { format: 'all' });
402
+ const validation = await validateCartridge(dir);
403
+ const drift = await driftCartridge(dir);
404
+ console.log(JSON.stringify({ cartridge: path.relative(root, dir), created, render, validation: validation.ok, drift: drift.status }, null, 2));
405
+ return;
406
+ }
407
+ if (sub === 'render') {
408
+ const format = readFlagValue(args, '--format', 'all');
409
+ console.log(JSON.stringify(await renderCartridge(dir, { format }), null, 2));
298
410
  return;
299
411
  }
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');
412
+ if (sub === 'validate') {
413
+ const validation = await validateCartridge(dir);
414
+ console.log(JSON.stringify(validation, null, 2));
415
+ process.exitCode = validation.ok ? 0 : 2;
416
+ return;
417
+ }
418
+ if (sub === 'drift') {
419
+ const drift = await driftCartridge(dir);
420
+ console.log(JSON.stringify(drift, null, 2));
421
+ process.exitCode = drift.status === 'low' ? 0 : 2;
422
+ return;
423
+ }
424
+ if (sub === 'snapshot') {
425
+ await renderCartridge(dir, { format: 'all' });
426
+ console.log(JSON.stringify(await snapshotCartridge(dir), null, 2));
427
+ return;
428
+ }
429
+ console.error('Usage: sks gx init|render|validate|drift|snapshot');
430
+ process.exitCode = 1;
302
431
  }
303
432
 
304
433
  async function team(args) {
@@ -41,7 +41,7 @@ export function buildDecisionContract({ mission, schema, answers }) {
41
41
  if_e2e_unavailable: 'run_unit_or_integration_and_record_e2e_not_executed',
42
42
  if_dependency_needed: 'avoid_new_dependency_unless_allowed_by_contract',
43
43
  if_existing_behavior_conflict: 'preserve_existing_public_behavior',
44
- if_visual_cartridge_conflict: 'vgraph_json_wins_over_sheet_png',
44
+ if_visual_cartridge_conflict: 'vgraph_json_wins_over_rendered_gx_artifact',
45
45
  if_wiki_conflict: 'current_code_wins_over_wiki',
46
46
  if_low_confidence_claim: 'read_source_do_not_ask_user',
47
47
  if_unresolvable_optional_scope: 'defer_optional_subtask_and_complete_core_acceptance_criteria',
package/src/core/fsx.mjs CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
 
8
- export const PACKAGE_VERSION = '0.3.0';
8
+ export const PACKAGE_VERSION = '0.4.0';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -0,0 +1,352 @@
1
+ import path from 'node:path';
2
+ import { exists, nowIso, readJson, readText, sha256, writeJsonAtomic, writeTextAtomic } from './fsx.mjs';
3
+
4
+ const SVG_WIDTH = 1280;
5
+ const SVG_HEIGHT = 820;
6
+
7
+ function stableJson(value) {
8
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
9
+ if (value && typeof value === 'object') {
10
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
11
+ }
12
+ return JSON.stringify(value);
13
+ }
14
+
15
+ function escapeXml(value = '') {
16
+ return String(value)
17
+ .replace(/&/g, '&amp;')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;')
21
+ .replace(/'/g, '&apos;');
22
+ }
23
+
24
+ function slug(value = '') {
25
+ return String(value || 'item').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'item';
26
+ }
27
+
28
+ function shortText(value = '', max = 64) {
29
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
30
+ return text.length > max ? `${text.slice(0, max - 1)}...` : text;
31
+ }
32
+
33
+ function splitLabel(value = '', maxLine = 24, maxLines = 3) {
34
+ const words = String(value || '').replace(/\s+/g, ' ').trim().split(' ').filter(Boolean);
35
+ const lines = [];
36
+ let current = '';
37
+ for (const word of words.length ? words : ['Untitled']) {
38
+ if (!current) current = word;
39
+ else if (`${current} ${word}`.length <= maxLine) current = `${current} ${word}`;
40
+ else {
41
+ lines.push(current);
42
+ current = word;
43
+ }
44
+ if (lines.length === maxLines) break;
45
+ }
46
+ if (current && lines.length < maxLines) lines.push(current);
47
+ return lines.slice(0, maxLines);
48
+ }
49
+
50
+ function normalizeNode(node, index) {
51
+ const id = slug(node?.id || `node-${index + 1}`);
52
+ return {
53
+ ...node,
54
+ id,
55
+ label: node?.label || node?.title || id,
56
+ kind: node?.kind || node?.type || 'component',
57
+ layer: node?.layer || node?.group || 'default',
58
+ risk: node?.risk || 'normal',
59
+ status: node?.status || 'unknown'
60
+ };
61
+ }
62
+
63
+ function normalizeEdge(edge, index) {
64
+ return {
65
+ ...edge,
66
+ id: slug(edge?.id || `edge-${index + 1}`),
67
+ from: slug(edge?.from || edge?.source || ''),
68
+ to: slug(edge?.to || edge?.target || ''),
69
+ label: edge?.label || edge?.kind || ''
70
+ };
71
+ }
72
+
73
+ export function normalizeVGraph(vgraph = {}) {
74
+ const nodes = Array.isArray(vgraph.nodes) ? vgraph.nodes.map(normalizeNode) : [];
75
+ const edges = Array.isArray(vgraph.edges) ? vgraph.edges.map(normalizeEdge) : [];
76
+ return {
77
+ id: slug(vgraph.id || vgraph.name || 'architecture-atlas'),
78
+ title: vgraph.title || vgraph.name || vgraph.id || 'Architecture Atlas',
79
+ version: vgraph.version || 1,
80
+ nodes,
81
+ edges,
82
+ invariants: Array.isArray(vgraph.invariants) ? vgraph.invariants : [],
83
+ tests: Array.isArray(vgraph.tests) ? vgraph.tests : [],
84
+ risks: Array.isArray(vgraph.risks) ? vgraph.risks : []
85
+ };
86
+ }
87
+
88
+ export function vgraphHash(vgraph = {}) {
89
+ return sha256(stableJson(normalizeVGraph(vgraph)));
90
+ }
91
+
92
+ function nodePalette(node) {
93
+ if (node.risk === 'critical' || node.status === 'blocked') return { fill: '#fee2e2', stroke: '#b91c1c', text: '#3b0a0a' };
94
+ if (node.risk === 'high' || node.status === 'warn') return { fill: '#ffedd5', stroke: '#c2410c', text: '#431407' };
95
+ if (node.status === 'passed' || node.status === 'safe') return { fill: '#dcfce7', stroke: '#15803d', text: '#052e16' };
96
+ if (node.kind === 'guard' || node.kind === 'policy') return { fill: '#e0f2fe', stroke: '#0369a1', text: '#082f49' };
97
+ return { fill: '#f8fafc', stroke: '#475569', text: '#0f172a' };
98
+ }
99
+
100
+ function layoutNodes(nodes) {
101
+ if (!nodes.length) return new Map();
102
+ const layers = [...new Set(nodes.map((node) => node.layer))].sort();
103
+ const byLayer = new Map(layers.map((layer) => [layer, nodes.filter((node) => node.layer === layer).sort((a, b) => a.id.localeCompare(b.id))]));
104
+ const positions = new Map();
105
+ const top = 170;
106
+ const bottom = 530;
107
+ const left = 92;
108
+ const right = 1188;
109
+ const layerGap = layers.length > 1 ? (bottom - top) / (layers.length - 1) : 0;
110
+ for (let li = 0; li < layers.length; li++) {
111
+ const layer = layers[li];
112
+ const row = byLayer.get(layer);
113
+ const y = layers.length > 1 ? top + li * layerGap : 300;
114
+ const gap = row.length > 1 ? (right - left) / (row.length - 1) : 0;
115
+ for (let i = 0; i < row.length; i++) {
116
+ const node = row[i];
117
+ const x = row.length > 1 ? left + i * gap : SVG_WIDTH / 2;
118
+ positions.set(node.id, { x, y, w: 196, h: 82, layer });
119
+ }
120
+ }
121
+ return positions;
122
+ }
123
+
124
+ function renderList(items, x, y, title) {
125
+ const lines = [`<text x="${x}" y="${y}" class="section-title">${escapeXml(title)}</text>`];
126
+ if (!items.length) {
127
+ lines.push(`<text x="${x}" y="${y + 34}" class="muted">No entries</text>`);
128
+ return lines.join('\n');
129
+ }
130
+ for (let i = 0; i < Math.min(items.length, 5); i++) {
131
+ const item = items[i];
132
+ const label = typeof item === 'string' ? item : (item.label || item.id || item.title || JSON.stringify(item));
133
+ lines.push(`<text x="${x}" y="${y + 34 + i * 28}" class="list-item">- ${escapeXml(shortText(label, 72))}</text>`);
134
+ }
135
+ if (items.length > 5) lines.push(`<text x="${x}" y="${y + 34 + 5 * 28}" class="muted">+ ${items.length - 5} more</text>`);
136
+ return lines.join('\n');
137
+ }
138
+
139
+ export function renderVGraphSvg(vgraph = {}, beta = {}) {
140
+ const graph = normalizeVGraph(vgraph);
141
+ const hash = vgraphHash(graph);
142
+ const positions = layoutNodes(graph.nodes);
143
+ const layers = [...new Set(graph.nodes.map((node) => node.layer))].sort();
144
+ const generatedAt = nowIso();
145
+ const edgeLines = graph.edges.map((edge) => {
146
+ const from = positions.get(edge.from);
147
+ const to = positions.get(edge.to);
148
+ if (!from || !to) return '';
149
+ const x1 = from.x;
150
+ const y1 = from.y + from.h / 2;
151
+ const x2 = to.x;
152
+ const y2 = to.y - to.h / 2;
153
+ const midX = (x1 + x2) / 2;
154
+ const midY = (y1 + y2) / 2;
155
+ return `<g class="edge">
156
+ <line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" marker-end="url(#arrow)"/>
157
+ ${edge.label ? `<text x="${midX}" y="${midY - 8}" class="edge-label">${escapeXml(shortText(edge.label, 28))}</text>` : ''}
158
+ </g>`;
159
+ }).join('\n');
160
+ const layerBands = layers.map((layer, index) => {
161
+ const y = layers.length > 1 ? 132 + index * (398 / Math.max(1, layers.length - 1)) : 250;
162
+ return `<text x="40" y="${y}" class="layer-label">${escapeXml(layer)}</text>`;
163
+ }).join('\n');
164
+ const nodeCards = graph.nodes.map((node) => {
165
+ const pos = positions.get(node.id);
166
+ const palette = nodePalette(node);
167
+ const labelLines = splitLabel(node.label);
168
+ const tag = `${node.kind} / ${node.status}`;
169
+ return `<g class="node" transform="translate(${pos.x - pos.w / 2} ${pos.y - pos.h / 2})">
170
+ <rect width="${pos.w}" height="${pos.h}" rx="14" fill="${palette.fill}" stroke="${palette.stroke}" stroke-width="3"/>
171
+ <text x="18" y="27" class="node-title" fill="${palette.text}">${escapeXml(labelLines[0])}</text>
172
+ ${labelLines.slice(1).map((line, i) => `<text x="18" y="${49 + i * 18}" class="node-title small" fill="${palette.text}">${escapeXml(line)}</text>`).join('\n')}
173
+ <text x="18" y="${pos.h - 14}" class="node-meta" fill="${palette.text}">${escapeXml(shortText(tag, 34))}</text>
174
+ </g>`;
175
+ }).join('\n');
176
+ const emptyState = graph.nodes.length ? '' : `<g class="empty">
177
+ <rect x="332" y="214" width="616" height="178" rx="18"/>
178
+ <text x="640" y="291" text-anchor="middle" class="empty-title">No graph nodes defined</text>
179
+ <text x="640" y="329" text-anchor="middle" class="muted">Edit vgraph.json, then run sks gx render ${escapeXml(graph.id)}</text>
180
+ </g>`;
181
+
182
+ return `<?xml version="1.0" encoding="UTF-8"?>
183
+ <svg xmlns="http://www.w3.org/2000/svg" width="${SVG_WIDTH}" height="${SVG_HEIGHT}" viewBox="0 0 ${SVG_WIDTH} ${SVG_HEIGHT}" role="img" aria-labelledby="title desc" data-generator="sneakoscope-codex" data-vgraph-id="${escapeXml(graph.id)}" data-vgraph-hash="${hash}" data-generated-at="${generatedAt}">
184
+ <title id="title">${escapeXml(graph.title)}</title>
185
+ <desc id="desc">Deterministic visual context sheet generated from vgraph.json.</desc>
186
+ <defs>
187
+ <marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto" markerUnits="strokeWidth">
188
+ <path d="M2,2 L10,6 L2,10 Z" fill="#475569"/>
189
+ </marker>
190
+ <style>
191
+ .title { font: 800 38px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
192
+ .subtitle, .muted { font: 500 18px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #64748b; }
193
+ .section-title { font: 800 24px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
194
+ .list-item { font: 500 18px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #334155; }
195
+ .layer-label { font: 800 17px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #94a3b8; text-transform: uppercase; }
196
+ .node-title { font: 800 17px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
197
+ .node-title.small { font-size: 15px; font-weight: 700; }
198
+ .node-meta { font: 700 12px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; opacity: .72; text-transform: uppercase; }
199
+ .edge line { stroke: #475569; stroke-width: 2.5; }
200
+ .edge-label { font: 700 13px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #475569; paint-order: stroke; stroke: #f8fafc; stroke-width: 5px; }
201
+ .empty rect { fill: #f8fafc; stroke: #cbd5e1; stroke-width: 2; }
202
+ .empty-title { font: 800 26px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #334155; }
203
+ </style>
204
+ </defs>
205
+ <rect width="${SVG_WIDTH}" height="${SVG_HEIGHT}" fill="#ffffff"/>
206
+ <rect x="0" y="0" width="${SVG_WIDTH}" height="108" fill="#f8fafc"/>
207
+ <text x="42" y="56" class="title">${escapeXml(graph.title)}</text>
208
+ <text x="42" y="86" class="subtitle">vgraph:${escapeXml(graph.id)} hash:${hash.slice(0, 12)} nodes:${graph.nodes.length} edges:${graph.edges.length} generated:${generatedAt}</text>
209
+ ${layerBands}
210
+ ${edgeLines}
211
+ ${nodeCards}
212
+ ${emptyState}
213
+ <line x1="42" y1="590" x2="1238" y2="590" stroke="#e2e8f0" stroke-width="2"/>
214
+ ${renderList(graph.invariants, 52, 638, 'Invariants')}
215
+ ${renderList(graph.tests, 690, 638, 'Tests')}
216
+ <text x="52" y="784" class="muted">Source: vgraph.json. Layout/read-order: ${escapeXml(beta?.id || graph.id)}. Renderer: Sneakoscope Codex deterministic GX.</text>
217
+ </svg>
218
+ `;
219
+ }
220
+
221
+ export function renderVGraphHtml(vgraph = {}, beta = {}, svg = renderVGraphSvg(vgraph, beta)) {
222
+ const graph = normalizeVGraph(vgraph);
223
+ return `<!doctype html>
224
+ <html lang="en">
225
+ <head>
226
+ <meta charset="utf-8">
227
+ <meta name="viewport" content="width=device-width, initial-scale=1">
228
+ <title>${escapeXml(graph.title)} - Sneakoscope Codex</title>
229
+ <style>
230
+ body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f1f5f9; color: #0f172a; }
231
+ main { max-width: 1320px; margin: 0 auto; padding: 24px; }
232
+ .sheet { background: white; border: 1px solid #cbd5e1; overflow: auto; }
233
+ svg { display: block; width: 100%; height: auto; }
234
+ pre { padding: 16px; background: #0f172a; color: #e2e8f0; overflow: auto; }
235
+ </style>
236
+ </head>
237
+ <body>
238
+ <main>
239
+ <div class="sheet">${svg.replace(/^<\?xml[^>]*>\s*/, '')}</div>
240
+ <h2>Context Packet</h2>
241
+ <pre>${escapeXml(JSON.stringify({ vgraph: normalizeVGraph(vgraph), beta }, null, 2))}</pre>
242
+ </main>
243
+ </body>
244
+ </html>
245
+ `;
246
+ }
247
+
248
+ export function validateVGraph(vgraph = {}, beta = {}) {
249
+ const graph = normalizeVGraph(vgraph);
250
+ const issues = [];
251
+ const warnings = [];
252
+ const ids = new Set();
253
+ if (!graph.id) issues.push({ id: 'missing_graph_id', severity: 'error', reason: 'vgraph.id is required.' });
254
+ for (const node of graph.nodes) {
255
+ if (ids.has(node.id)) issues.push({ id: 'duplicate_node_id', severity: 'error', node: node.id, reason: 'Node ids must be unique.' });
256
+ ids.add(node.id);
257
+ if (!node.label) warnings.push({ id: 'missing_node_label', severity: 'warning', node: node.id, reason: 'Node has no label.' });
258
+ }
259
+ for (const edge of graph.edges) {
260
+ if (!ids.has(edge.from)) issues.push({ id: 'missing_edge_source', severity: 'error', edge: edge.id, from: edge.from });
261
+ if (!ids.has(edge.to)) issues.push({ id: 'missing_edge_target', severity: 'error', edge: edge.id, to: edge.to });
262
+ }
263
+ if (beta?.read_order && !Array.isArray(beta.read_order)) issues.push({ id: 'invalid_read_order', severity: 'error', reason: 'beta.read_order must be an array when present.' });
264
+ if (!graph.nodes.length) warnings.push({ id: 'empty_graph', severity: 'warning', reason: 'vgraph contains no nodes.' });
265
+ return {
266
+ checked_at: nowIso(),
267
+ ok: issues.length === 0,
268
+ graph_id: graph.id,
269
+ source_hash: vgraphHash(graph),
270
+ counts: { nodes: graph.nodes.length, edges: graph.edges.length, invariants: graph.invariants.length, tests: graph.tests.length },
271
+ issues,
272
+ warnings
273
+ };
274
+ }
275
+
276
+ function extractRenderHash(text = '') {
277
+ const match = String(text).match(/\bdata-vgraph-hash="([^"]+)"/);
278
+ return match ? match[1] : null;
279
+ }
280
+
281
+ export async function renderCartridge(dir, { format = 'all' } = {}) {
282
+ const vgraph = await readJson(path.join(dir, 'vgraph.json'));
283
+ const beta = await readJson(path.join(dir, 'beta.json'), {});
284
+ const svg = renderVGraphSvg(vgraph, beta);
285
+ const outputs = [];
286
+ if (format === 'all' || format === 'svg') {
287
+ await writeTextAtomic(path.join(dir, 'render.svg'), svg);
288
+ outputs.push('render.svg');
289
+ }
290
+ if (format === 'all' || format === 'html') {
291
+ await writeTextAtomic(path.join(dir, 'render.html'), renderVGraphHtml(vgraph, beta, svg));
292
+ outputs.push('render.html');
293
+ }
294
+ if (!outputs.length) throw new Error('Unsupported GX render format. Use svg, html, or all.');
295
+ return { graph_id: normalizeVGraph(vgraph).id, source_hash: vgraphHash(vgraph), outputs };
296
+ }
297
+
298
+ export async function validateCartridge(dir) {
299
+ const vgraph = await readJson(path.join(dir, 'vgraph.json'));
300
+ const beta = await readJson(path.join(dir, 'beta.json'), {});
301
+ const validation = validateVGraph(vgraph, beta);
302
+ await writeJsonAtomic(path.join(dir, 'validation.json'), validation);
303
+ return validation;
304
+ }
305
+
306
+ export async function driftCartridge(dir) {
307
+ const vgraph = await readJson(path.join(dir, 'vgraph.json'));
308
+ const sourceHash = vgraphHash(vgraph);
309
+ const renderPath = path.join(dir, 'render.svg');
310
+ const renderText = await readText(renderPath, '');
311
+ const renderHash = renderText ? extractRenderHash(renderText) : null;
312
+ const validation = validateVGraph(vgraph, await readJson(path.join(dir, 'beta.json'), {}));
313
+ const reasons = [];
314
+ if (!renderText) reasons.push('render_svg_missing');
315
+ if (renderText && !renderHash) reasons.push('render_svg_missing_vgraph_hash');
316
+ if (renderHash && renderHash !== sourceHash) reasons.push('render_svg_stale');
317
+ for (const issue of validation.issues) reasons.push(`validation:${issue.id}`);
318
+ const drift = {
319
+ checked_at: nowIso(),
320
+ status: reasons.length ? 'high' : 'low',
321
+ source_hash: sourceHash,
322
+ render_hash: renderHash,
323
+ reasons,
324
+ validation
325
+ };
326
+ await writeJsonAtomic(path.join(dir, 'drift.json'), drift);
327
+ return drift;
328
+ }
329
+
330
+ export async function snapshotCartridge(dir) {
331
+ const vgraph = await readJson(path.join(dir, 'vgraph.json'));
332
+ const beta = await readJson(path.join(dir, 'beta.json'), {});
333
+ const validation = await validateCartridge(dir);
334
+ const drift = await driftCartridge(dir);
335
+ const snapshot = {
336
+ created_at: nowIso(),
337
+ graph_id: normalizeVGraph(vgraph).id,
338
+ source_hash: vgraphHash(vgraph),
339
+ files: {
340
+ vgraph: 'vgraph.json',
341
+ beta: 'beta.json',
342
+ svg: await exists(path.join(dir, 'render.svg')) ? 'render.svg' : null,
343
+ html: await exists(path.join(dir, 'render.html')) ? 'render.html' : null
344
+ },
345
+ validation,
346
+ drift,
347
+ vgraph: normalizeVGraph(vgraph),
348
+ beta
349
+ };
350
+ await writeJsonAtomic(path.join(dir, 'snapshot.json'), snapshot);
351
+ return snapshot;
352
+ }
package/src/core/init.mjs CHANGED
@@ -22,8 +22,8 @@ Sneakoscope Codex keeps runtime state bounded. Do not write large raw logs into
22
22
  2. decision-contract.json
23
23
  3. vgraph.json
24
24
  4. beta.json
25
- 5. LLM Wiki
26
- 6. visual parse
25
+ 5. GX render/snapshot metadata
26
+ 6. LLM Wiki
27
27
  7. model knowledge only if explicitly allowed
28
28
 
29
29
  ## Database Safety
@@ -50,7 +50,8 @@ export async function initProject(root, opts = {}) {
50
50
  no_external_tools: true,
51
51
  codex_required: true,
52
52
  native_runtime_dependencies: 0,
53
- database_safety: 'destructive_db_operations_denied_always'
53
+ database_safety: 'destructive_db_operations_denied_always',
54
+ gx_renderer: 'deterministic_svg_html'
54
55
  });
55
56
  created.push('.sneakoscope/manifest.json');
56
57
 
@@ -82,6 +83,11 @@ export async function initProject(root, opts = {}) {
82
83
  live_database_mode: 'read_only',
83
84
  destructive_operations_allowed: false,
84
85
  mcp_write_tools_allowed: false
86
+ },
87
+ gx: {
88
+ renderer: 'deterministic_svg_html',
89
+ source_of_truth: 'vgraph.json',
90
+ external_image_generation: false
85
91
  }
86
92
  });
87
93
  created.push('.sneakoscope/policy.json');
@@ -120,11 +126,11 @@ async function installSkills(root) {
120
126
  'ralph-supervisor': `---\nname: ralph-supervisor\ndescription: Run the Ralph no-question loop after a decision contract is sealed.\n---\n\nYou are the Ralph Supervisor.\n\nRules:\n- Never ask the user during Ralph run.\n- Use decision-contract.json and the decision ladder.\n- Continue until done-gate.json passes or safe scope is completed with explicit limitation.\n- Keep outputs bounded. Write raw logs to files and summarize only tails.\n- Database destructive operations are never allowed.\n- Write progress to .sneakoscope mission files.\n`,
121
127
  'ralph-resolver': `---\nname: ralph-resolver\ndescription: Resolve newly discovered ambiguity during Ralph using the sealed decision ladder, without asking the user.\n---\n\nResolve ambiguity in this order: seed contract, explicit answers, approved defaults, AGENTS.md, current code/tests, smallest reversible change, defer optional scope. Never ask the user. If database risk is involved, prefer read-only, no-op, local-only migration file, or safe limitation; never run destructive SQL.\n`,
122
128
  'hproof-claim-ledger': `---\nname: hproof-claim-ledger\ndescription: Extract atomic claims and classify support status.\n---\n\nEvery factual statement must become an atomic claim. Unsupported critical claims cannot be used for implementation or final answer. Database claims require DB safety evidence.\n`,
123
- 'hproof-evidence-bind': `---\nname: hproof-evidence-bind\ndescription: Bind claims to code, tests, decision contract, vgraph, beta, wiki, or visual parse evidence.\n---\n\nEvidence priority: current code/tests, decision-contract.json, vgraph.json, beta.json, wiki, visual parse, user prompt. Database claims must respect .sneakoscope/db-safety.json.\n`,
129
+ 'hproof-evidence-bind': `---\nname: hproof-evidence-bind\ndescription: Bind claims to code, tests, decision contract, vgraph, beta, wiki, or GX render evidence.\n---\n\nEvidence priority: current code/tests, decision-contract.json, vgraph.json, beta.json, GX snapshot/render metadata, wiki, user prompt. Database claims must respect .sneakoscope/db-safety.json.\n`,
124
130
  'db-safety-guard': `---\nname: db-safety-guard\ndescription: Enforce Sneakoscope Codex database safety before using SQL, Supabase MCP, Postgres, Prisma, Drizzle, Knex, or migration commands.\n---\n\nRules:\n- Never run DROP, TRUNCATE, mass DELETE/UPDATE, db reset, db push, project deletion, branch reset/merge/delete, or RLS-disabling operations.\n- Supabase MCP must be read-only and project-scoped by default.\n- Live writes through execute_sql are blocked; use migration files and only local/preview branches if explicitly allowed.\n- Production writes are forbidden.\n- If unsure, read-only only.\n`,
125
- 'gx-visual-generate': `---\nname: gx-visual-generate\ndescription: Generate a visual sheet using GPT Image 2 from vgraph.json and beta.json.\n---\n\nUse built-in GPT Image 2 / $imagegen only. Do not use external diagram tools. vgraph.json is source of truth.\n`,
126
- 'gx-visual-read': `---\nname: gx-visual-read\ndescription: Read a Sneakoscope Codex visual sheet and produce parse.json.\n---\n\nExtract nodes, edges, invariants, tests, risks, and uncertainties. Do not infer hidden nodes.\n`,
127
- 'gx-visual-validate': `---\nname: gx-visual-validate\ndescription: Validate visual parse against vgraph.json and beta.json.\n---\n\nIf critical nodes, edges, or invariants are missing, mark validation failed.\n`,
131
+ 'gx-visual-generate': `---\nname: gx-visual-generate\ndescription: Render a deterministic SVG/HTML visual sheet from vgraph.json and beta.json.\n---\n\nUse sks gx render. Do not use external image generation. vgraph.json is the source of truth and the SVG embeds its source hash.\n`,
132
+ 'gx-visual-read': `---\nname: gx-visual-read\ndescription: Read a Sneakoscope Codex deterministic visual sheet and produce context notes.\n---\n\nExtract nodes, edges, invariants, tests, risks, and uncertainties from vgraph.json, beta.json, render.svg, or snapshot.json. Do not infer hidden nodes.\n`,
133
+ 'gx-visual-validate': `---\nname: gx-visual-validate\ndescription: Validate render metadata against vgraph.json and beta.json.\n---\n\nRun sks gx validate and sks gx drift. If critical nodes, edges, or invariants are missing or the render hash is stale, mark validation failed.\n`,
128
134
  'turbo-context-pack': `---\nname: turbo-context-pack\ndescription: Build ultra-low-token context packet with Q4 bits, Q3 tags, top-K claims, and minimal evidence.\n---\n\nDefault to Q4/Q3 only. Add Q2 or Q1 only when needed for support or verification.\n`
129
135
  };
130
136
  for (const [name, content] of Object.entries(skills)) {