sneakoscope 0.9.11 → 0.9.13

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.
Files changed (62) hide show
  1. package/README.md +28 -2
  2. package/crates/sks-core/Cargo.lock +7 -0
  3. package/crates/sks-core/Cargo.toml +10 -0
  4. package/crates/sks-core/src/main.rs +202 -0
  5. package/package.json +15 -3
  6. package/src/cli/args.mjs +49 -0
  7. package/src/cli/command-registry.mjs +128 -0
  8. package/src/cli/feature-commands.mjs +112 -6
  9. package/src/cli/install-helpers.mjs +14 -7
  10. package/src/cli/legacy-main.mjs +4147 -0
  11. package/src/cli/main.mjs +7 -4138
  12. package/src/cli/output.mjs +9 -0
  13. package/src/cli/router.mjs +30 -0
  14. package/src/commands/all-features.mjs +6 -0
  15. package/src/commands/codex-app.mjs +30 -0
  16. package/src/commands/codex-lb.mjs +47 -0
  17. package/src/commands/db.mjs +6 -0
  18. package/src/commands/doctor.mjs +46 -0
  19. package/src/commands/features.mjs +6 -0
  20. package/src/commands/help.mjs +77 -0
  21. package/src/commands/hooks.mjs +6 -0
  22. package/src/commands/perf.mjs +91 -0
  23. package/src/commands/proof.mjs +103 -0
  24. package/src/commands/root.mjs +24 -0
  25. package/src/commands/version.mjs +5 -0
  26. package/src/commands/wiki.mjs +95 -0
  27. package/src/core/codex-lb-circuit.mjs +130 -0
  28. package/src/core/db-safety.mjs +18 -3
  29. package/src/core/feature-fixtures.mjs +103 -0
  30. package/src/core/feature-registry.mjs +117 -11
  31. package/src/core/fsx.mjs +1 -1
  32. package/src/core/hooks-runtime.mjs +17 -6
  33. package/src/core/language-preference.mjs +106 -0
  34. package/src/core/pipeline.mjs +24 -0
  35. package/src/core/proof/claim-ledger.mjs +9 -0
  36. package/src/core/proof/command-ledger.mjs +17 -0
  37. package/src/core/proof/evidence-collector.mjs +33 -0
  38. package/src/core/proof/file-change-ledger.mjs +6 -0
  39. package/src/core/proof/proof-reader.mjs +30 -0
  40. package/src/core/proof/proof-redaction.test-helper.mjs +9 -0
  41. package/src/core/proof/proof-schema.mjs +42 -0
  42. package/src/core/proof/proof-writer.mjs +81 -0
  43. package/src/core/proof/route-adapter.mjs +74 -0
  44. package/src/core/proof/route-proof-gate.mjs +33 -0
  45. package/src/core/proof/route-proof-policy.mjs +96 -0
  46. package/src/core/proof/selftest-proof-fixtures.mjs +54 -0
  47. package/src/core/proof/validation.mjs +20 -0
  48. package/src/core/routes.mjs +4 -3
  49. package/src/core/rust-accelerator.mjs +55 -3
  50. package/src/core/secret-redaction.mjs +69 -0
  51. package/src/core/version-manager.mjs +11 -7
  52. package/src/core/version.mjs +1 -0
  53. package/src/core/wiki-image/bbox.mjs +10 -0
  54. package/src/core/wiki-image/callout-parser.mjs +16 -0
  55. package/src/core/wiki-image/computer-use-ledger.mjs +38 -0
  56. package/src/core/wiki-image/image-hash.mjs +42 -0
  57. package/src/core/wiki-image/image-relation.mjs +2 -0
  58. package/src/core/wiki-image/image-voxel-ledger.mjs +147 -0
  59. package/src/core/wiki-image/image-voxel-schema.mjs +16 -0
  60. package/src/core/wiki-image/proof-linker.mjs +12 -0
  61. package/src/core/wiki-image/validation.mjs +53 -0
  62. package/src/core/wiki-image/visual-anchor.mjs +42 -0
@@ -0,0 +1,106 @@
1
+ const HANGUL_RE = /[\u3131-\u318e\uac00-\ud7a3]/gu;
2
+ const LATIN_WORD_RE = /[A-Za-z][A-Za-z']*/g;
3
+
4
+ const KOREAN_OVERRIDE_PATTERNS = [
5
+ /\b(?:answer|respond|reply|write|explain|summari[sz]e|use)\s+(?:in\s+)?(?:korean|hangul)\b/i,
6
+ /(?:한국어|한글)(?:로|으로)?\s*(?:답|응답|말|작성|설명|정리|써|해줘|해주세요)/i,
7
+ /(?:답|응답|말|작성|설명|정리|써)\S{0,12}(?:한국어|한글)(?:로|으로)?/i
8
+ ];
9
+
10
+ const ENGLISH_OVERRIDE_PATTERNS = [
11
+ /\b(?:answer|respond|reply|write|explain|summari[sz]e|use)\s+(?:in\s+)?english\b/i,
12
+ /영어(?:로|으로)?\s*(?:답|응답|말|작성|설명|정리|써|해줘|해주세요)/i,
13
+ /(?:답|응답|말|작성|설명|정리|써)\S{0,12}영어(?:로|으로)?/i
14
+ ];
15
+
16
+ function countMatches(text, pattern) {
17
+ return (String(text || '').match(pattern) || []).length;
18
+ }
19
+
20
+ function hasExplicitOverride(text, patterns) {
21
+ return patterns.some((pattern) => pattern.test(text));
22
+ }
23
+
24
+ export function detectResponseLanguage(prompt = '') {
25
+ const text = String(prompt || '').trim();
26
+ if (!text) {
27
+ return { code: 'unknown', label: 'unknown', confidence: 0, reason: 'empty_prompt' };
28
+ }
29
+
30
+ const koreanOverride = hasExplicitOverride(text, KOREAN_OVERRIDE_PATTERNS);
31
+ const englishOverride = hasExplicitOverride(text, ENGLISH_OVERRIDE_PATTERNS);
32
+ if (koreanOverride && !englishOverride) {
33
+ return { code: 'ko', label: 'Korean', confidence: 1, reason: 'explicit_korean_override' };
34
+ }
35
+ if (englishOverride && !koreanOverride) {
36
+ return { code: 'en', label: 'English', confidence: 1, reason: 'explicit_english_override' };
37
+ }
38
+
39
+ const hangulCount = countMatches(text, HANGUL_RE);
40
+ const latinWords = String(text || '').match(LATIN_WORD_RE) || [];
41
+ const latinCharCount = latinWords.join('').length;
42
+ const totalLanguageChars = hangulCount + latinCharCount;
43
+ const hangulRatio = totalLanguageChars > 0 ? hangulCount / totalLanguageChars : 0;
44
+
45
+ if (hangulCount >= 8 || (hangulCount >= 2 && hangulRatio >= 0.08)) {
46
+ return {
47
+ code: 'ko',
48
+ label: 'Korean',
49
+ confidence: Math.min(0.98, 0.62 + hangulRatio),
50
+ reason: 'hangul_dominant_or_present'
51
+ };
52
+ }
53
+
54
+ if (latinWords.length >= 2) {
55
+ return {
56
+ code: 'en',
57
+ label: 'English',
58
+ confidence: Math.min(0.95, 0.55 + latinWords.length / 40),
59
+ reason: 'latin_words_present'
60
+ };
61
+ }
62
+
63
+ return { code: 'unknown', label: 'unknown', confidence: 0.2, reason: 'insufficient_language_signal' };
64
+ }
65
+
66
+ export function responseLanguageInstruction(prompt = '') {
67
+ const language = detectResponseLanguage(prompt);
68
+ if (language.code === 'ko') {
69
+ return [
70
+ '응답 언어: 사용자 요청은 주로 한국어입니다.',
71
+ '진행 업데이트, 사용자에게 보이는 요약, 최종 완료 요약, SKS 솔직모드는 한국어로 작성하세요.',
72
+ '코드, 명령어, 파일 경로, 패키지명, API명, 인용 원문은 원래 언어 그대로 유지하세요.',
73
+ '이후 사용자 메시지가 다른 응답 언어를 명시하면 가장 최근의 명시적 언어 요청을 따르세요.'
74
+ ].join(' ');
75
+ }
76
+ if (language.code === 'en') {
77
+ return [
78
+ 'Response language: the user prompt is primarily English.',
79
+ 'Write assistant progress updates, user-visible summaries, final completion summary, and SKS Honest Mode in English.',
80
+ 'Preserve code, commands, file paths, package names, API names, and quoted source text in their original language.',
81
+ 'If a later user message explicitly asks for a different response language, follow the latest explicit language request.'
82
+ ].join(' ');
83
+ }
84
+ return [
85
+ 'Response language: match the user prompt language when clear.',
86
+ 'If the prompt language remains ambiguous, use concise English while preserving code, commands, file paths, package names, API names, and quoted source text as-is.'
87
+ ].join(' ');
88
+ }
89
+
90
+ export function localizedFinalizationReason(kind, prompt = '') {
91
+ const language = detectResponseLanguage(prompt);
92
+ const korean = language.code === 'ko';
93
+ if (kind === 'completion_summary_missing') {
94
+ return korean
95
+ ? 'SKS 최종 완료 요약(completion summary)이 필요합니다. 마치기 전에 무엇을 했는지, 사용자/레포에 무엇이 바뀌었는지, 무엇을 검증했는지, 남은 gap이 무엇인지 SKS 솔직모드와 함께 한국어로 설명하세요.'
96
+ : 'SKS final completion summary is required before finishing. Explain what was done, what changed for the user/repo, what was verified, and any remaining gaps before or alongside SKS Honest Mode.';
97
+ }
98
+ if (kind === 'honest_loopback') {
99
+ return korean
100
+ ? 'SKS 솔직모드에서 해결되지 않은 gap이 발견되었습니다. decision-contract.json 기준의 post-ambiguity execution phase에서 계속 진행하고, gap을 고친 뒤 검증을 다시 실행하고, TriWiki를 refresh/validate한 다음 최종 솔직모드를 다시 시도하세요.'
101
+ : 'SKS Honest Mode found unresolved gaps. Continue from the post-ambiguity execution phase using decision-contract.json, fix them, rerun verification, refresh/validate TriWiki, then retry final Honest Mode.';
102
+ }
103
+ return korean
104
+ ? '마치기 전에 SKS 솔직모드가 필요합니다. 실제 목표를 다시 확인하고, 증거/테스트를 검증하고, 남은 gap을 솔직히 적은 뒤 최종 답변을 한국어로 제공하세요. 짧은 "SKS 솔직모드" 또는 "솔직모드" 섹션을 포함하세요.'
105
+ : 'SKS Honest Mode is required before finishing. Re-check the actual goal, verify evidence/tests, state gaps honestly, and only then provide the final answer. Include a short "SKS Honest Mode" or "솔직모드" section.';
106
+ }
@@ -16,7 +16,10 @@ import { evaluateResearchGate, writeResearchPlan } from './research.mjs';
16
16
  import { PPT_REQUIRED_GATE_FIELDS, writePptRouteArtifacts } from './ppt.mjs';
17
17
  import { writeQaLoopArtifacts } from './qa-loop.mjs';
18
18
  import { IMAGE_UX_REVIEW_GATE_ARTIFACT, IMAGE_UX_REVIEW_POLICY_ARTIFACT, IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT, IMAGE_UX_REVIEW_REQUIRED_GATE_FIELDS, writeImageUxReviewRouteArtifacts } from './image-ux-review.mjs';
19
+ import { responseLanguageInstruction } from './language-preference.mjs';
19
20
  import { SPEED_LANE_POLICY } from './proof-field.mjs';
21
+ import { validateRouteCompletionProof } from './proof/route-proof-gate.mjs';
22
+ import { routeFromState, routeRequiresCompletionProof } from './proof/route-proof-policy.mjs';
20
23
  import { permissionGateSummary } from './permission-gates.mjs';
21
24
  import { CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, SOLUTION_SCOUT_STAGE_ID, chatCaptureIntakeText, context7RequirementText, dollarCommand, evidenceMentionsForbiddenBrowserAutomation, getdesignReferencePolicyText, hasFromChatImgSignal, hasMadSksSignal, imageUxReviewPipelinePolicyText, looksLikeProblemSolvingRequest, noUnrequestedFallbackCodePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, reflectionRequiredForRoute, reasoningInstruction, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, solutionScoutPolicyText, speedLanePolicyText, stripDollarCommand, stripMadSksSignal, stripVisibleDecisionAnswerBlocks, subagentExecutionPolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
22
25
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from './team-dag.mjs';
@@ -313,6 +316,7 @@ export function promptPipelineContext(prompt, route = null) {
313
316
  reasoningInstruction(reasoning),
314
317
  'Before work, load the required SKS skill context and follow the route lifecycle instead of treating the command as plain text.',
315
318
  'Codex App visibility: briefly surface what SKS is doing before tools run, mirror important worker/tool status to mission artifacts, and keep progress legible to the user.',
319
+ responseLanguageInstruction(cleanPrompt),
316
320
  'Hook visibility limit: hooks can inject context/status or block/continue a turn, but they cannot create arbitrary live chat bubbles; use team events, mission files, or normal assistant updates for live transcript details.',
317
321
  'Ambient Goal continuation: even without an explicit $Goal keyword, use Codex native /goal persistence when it helps keep long work resumable and complete; do not let it replace or skip the selected SKS route gates.',
318
322
  'Route contract: execution routes infer contract answers from the prompt, TriWiki/current-code defaults, and conservative SKS policy. DFix and Answer bypass stateful execution because they do not start implementation.',
@@ -355,6 +359,7 @@ export function dfixQuickContext(prompt, route = routePrompt(prompt)) {
355
359
  const routeLabel = route?.command || '$DFix';
356
360
  return [
357
361
  `DFix ultralight pipeline active. Route: ${routeLabel} (Direct Fix: tiny copy/config/docs/labels/spacing/translation/simple mechanical edits).`,
362
+ responseLanguageInstruction(task),
358
363
  'Bypass: do not enter the general SKS prompt pipeline, mission creation, ambiguity gate, TriWiki refresh, Context7 routing, subagent orchestration, Goal, Research, eval, or broad planning.',
359
364
  `Task: ${task}`,
360
365
  'Task list:',
@@ -371,6 +376,7 @@ export function answerOnlyContext(prompt, route = routePrompt(prompt)) {
371
376
  const required = routeNeedsContext7(route, task);
372
377
  return [
373
378
  `SKS answer-only pipeline active. Route: ${route?.command || '$Answer'} (${route?.route || 'answer-only research'}).`,
379
+ responseLanguageInstruction(task),
374
380
  'Intent classification: answer/research question, not implementation. Do not create route mission state, ask ambiguity-gate questions, spawn subagents, continue active Team/Goal work, or edit files unless the user explicitly asks for implementation.',
375
381
  `Question: ${task}`,
376
382
  'Evidence flow:',
@@ -464,6 +470,7 @@ export function computerUseFastContext(prompt, route = routePrompt(prompt)) {
464
470
  const task = stripDollarCommand(prompt) || String(prompt || '').trim();
465
471
  return [
466
472
  `Computer Use fast lane active. Route: ${route?.command || '$Computer-Use'} (${route?.route || 'Computer Use fast lane'}).`,
473
+ responseLanguageInstruction(task),
467
474
  'Speed contract: do not enter Team, QA-LOOP clarification, repeated upfront TriWiki refresh, Context7, subagent orchestration, debate, reflection, or broad planning unless the user explicitly requests that heavier route.',
468
475
  `Task: ${task}`,
469
476
  'Execution order:',
@@ -482,6 +489,7 @@ async function prepareWikiQuickRoute(route, task) {
482
489
  route,
483
490
  additionalContext: [
484
491
  `SKS wiki pipeline active. Route: ${route.command} (${route.route}).`,
492
+ responseLanguageInstruction(task),
485
493
  `Task: ${task || 'refresh and validate TriWiki'}`,
486
494
  'Run policy: refresh/update/갱신 -> `sks wiki refresh` then validate; prune/clean/정리 -> `sks wiki refresh --prune` or dry-run prune first; pack -> `sks wiki pack` then validate.',
487
495
  stackCurrentDocsPolicyText(),
@@ -1351,11 +1359,27 @@ export async function evaluateStop(root, state, payload, opts = {}) {
1351
1359
  return complianceBlock(root, state, `SKS ${state.route_command || state.mode} route cannot stop yet. Pass ${gate.file || state.stop_gate} or record a hard blocker with evidence before finishing.${missing}`, { gate: gate.file || state.stop_gate, missing: gate.missing });
1352
1360
  }
1353
1361
  }
1362
+ const proofGate = await routeProofGateStatus(root, state);
1363
+ if (!proofGate.ok) {
1364
+ return complianceBlock(root, state, `SKS ${state.route_command || state.mode || 'route'} route cannot finalize without a valid Completion Proof. Missing or invalid proof issues: ${proofGate.issues.join(', ')}.`, { gate: 'completion-proof', missing: proofGate.issues });
1365
+ }
1354
1366
  const reflection = await reflectionGateStatus(root, state);
1355
1367
  if (!reflection.ok) return complianceBlock(root, state, reflectionStopReason(state, reflection), { gate: 'reflection', missing: reflection.missing });
1356
1368
  return null;
1357
1369
  }
1358
1370
 
1371
+ async function routeProofGateStatus(root, state = {}) {
1372
+ const route = routeFromState(state);
1373
+ const required = state.proof_required === true || routeRequiresCompletionProof(route);
1374
+ if (!required || !state?.mission_id) return { ok: true, required: false, issues: [] };
1375
+ return validateRouteCompletionProof(root, {
1376
+ missionId: state.mission_id,
1377
+ route,
1378
+ state,
1379
+ visualClaim: state.visual_claim !== false
1380
+ });
1381
+ }
1382
+
1359
1383
  function clarificationGatePending(state = {}) {
1360
1384
  const phase = String(state.phase || '');
1361
1385
  return Boolean(state?.clarification_required && phase.includes('CLARIFICATION_AWAITING_ANSWERS'))
@@ -0,0 +1,9 @@
1
+ export function summarizeClaims(claims = []) {
2
+ const rows = Array.isArray(claims) ? claims : [];
3
+ return {
4
+ total: rows.length,
5
+ supported: rows.filter((claim) => claim.status === 'supported').length,
6
+ unverified: rows.filter((claim) => claim.status === 'unverified').length,
7
+ blocked: rows.filter((claim) => claim.status === 'blocked').length
8
+ };
9
+ }
@@ -0,0 +1,17 @@
1
+ import path from 'node:path';
2
+ import { appendJsonl, nowIso, packageRoot, readText } from '../fsx.mjs';
3
+ import { redactSecrets } from '../secret-redaction.mjs';
4
+ import { proofDir } from './proof-writer.mjs';
5
+
6
+ export async function appendProofCommand(root = packageRoot(), command = {}) {
7
+ const record = redactSecrets({ ts: nowIso(), ...command });
8
+ await appendJsonl(path.join(proofDir(root), 'commands.jsonl'), record);
9
+ return record;
10
+ }
11
+
12
+ export async function readProofCommands(root = packageRoot()) {
13
+ const text = await readText(path.join(proofDir(root), 'commands.jsonl'), '');
14
+ return text.split(/\r?\n/).filter(Boolean).map((line) => {
15
+ try { return JSON.parse(line); } catch { return { raw: line }; }
16
+ });
17
+ }
@@ -0,0 +1,33 @@
1
+ import path from 'node:path';
2
+ import { packageRoot, readJson, runProcess, which } from '../fsx.mjs';
3
+ import { codexLbMetrics, readCodexLbCircuit } from '../codex-lb-circuit.mjs';
4
+ import { imageVoxelSummary } from '../wiki-image/image-voxel-ledger.mjs';
5
+
6
+ export async function collectProofEvidence(root = packageRoot()) {
7
+ return {
8
+ files: await collectGitFileChanges(root),
9
+ image_voxels: await imageVoxelSummary(root).catch(() => null),
10
+ triwiki: await readJson(path.join(root, '.sneakoscope', 'wiki', 'context-pack.json'), null).then((pack) => pack ? {
11
+ status: 'present',
12
+ schema: pack.schema || null,
13
+ claims: pack.trust_summary?.claims || pack.wiki?.a?.length || 0
14
+ } : null).catch(() => null),
15
+ codex_lb: await readCodexLbCircuit(root).then((circuit) => codexLbMetrics(circuit)).catch(() => null),
16
+ db_safety: await readJson(path.join(root, '.sneakoscope', 'db-safety.json'), null).then((policy) => policy ? {
17
+ status: 'present',
18
+ mode: policy.mode || null,
19
+ destructive_operations: policy.destructive_operations || null
20
+ } : null).catch(() => null)
21
+ };
22
+ }
23
+
24
+ async function collectGitFileChanges(root) {
25
+ const git = await which('git').catch(() => null);
26
+ if (!git) return [];
27
+ const result = await runProcess(git, ['status', '--short'], { cwd: root, timeoutMs: 5000, maxOutputBytes: 64 * 1024 }).catch(() => null);
28
+ if (!result || result.code !== 0) return [];
29
+ return result.stdout.split(/\r?\n/).filter(Boolean).map((line) => ({
30
+ status: line.slice(0, 2).trim() || 'changed',
31
+ path: line.slice(3).trim()
32
+ }));
33
+ }
@@ -0,0 +1,6 @@
1
+ import { collectProofEvidence } from './evidence-collector.mjs';
2
+
3
+ export async function fileChangeLedger(root) {
4
+ const evidence = await collectProofEvidence(root);
5
+ return evidence.files || [];
6
+ }
@@ -0,0 +1,30 @@
1
+ import path from 'node:path';
2
+ import { exists, packageRoot, readJson, readText } from '../fsx.mjs';
3
+ import { emptyCompletionProof } from './proof-schema.mjs';
4
+ import { proofDir } from './proof-writer.mjs';
5
+
6
+ export async function readLatestProof(root = packageRoot()) {
7
+ const file = path.join(proofDir(root), 'latest.json');
8
+ if (!await exists(file)) return emptyCompletionProof({
9
+ status: 'not_verified',
10
+ unverified: ['No completion proof has been written yet.']
11
+ });
12
+ return readJson(file);
13
+ }
14
+
15
+ export async function readLatestProofMarkdown(root = packageRoot()) {
16
+ const file = path.join(proofDir(root), 'latest.md');
17
+ if (!await exists(file)) return '# SKS Completion Proof\n\nNo completion proof has been written yet.\n';
18
+ return readText(file);
19
+ }
20
+
21
+ export async function readRouteProof(root = packageRoot(), missionId = null) {
22
+ if (missionId) {
23
+ const missionProof = path.join(root, '.sneakoscope', 'missions', missionId, 'completion-proof.json');
24
+ if (await exists(missionProof)) return readJson(missionProof);
25
+ return null;
26
+ }
27
+ const latest = path.join(proofDir(root), 'latest.json');
28
+ if (await exists(latest)) return readJson(latest);
29
+ return null;
30
+ }
@@ -0,0 +1,9 @@
1
+ import { containsPlaintextSecret, redactSecrets } from '../secret-redaction.mjs';
2
+
3
+ export function assertProofRedaction(value) {
4
+ const redacted = redactSecrets(value);
5
+ return {
6
+ ok: !containsPlaintextSecret(redacted),
7
+ redacted
8
+ };
9
+ }
@@ -0,0 +1,42 @@
1
+ import { PACKAGE_VERSION, nowIso } from '../fsx.mjs';
2
+
3
+ export const COMPLETION_PROOF_SCHEMA = 'sks.completion-proof.v1';
4
+ export const COMPLETION_PROOF_STATUSES = Object.freeze([
5
+ 'verified',
6
+ 'verified_partial',
7
+ 'blocked',
8
+ 'not_verified',
9
+ 'failed'
10
+ ]);
11
+
12
+ export function emptyCompletionProof(overrides = {}) {
13
+ return {
14
+ schema: COMPLETION_PROOF_SCHEMA,
15
+ version: PACKAGE_VERSION,
16
+ generated_at: nowIso(),
17
+ mission_id: null,
18
+ route: null,
19
+ status: 'not_verified',
20
+ summary: {
21
+ files_changed: 0,
22
+ commands_run: 0,
23
+ tests_passed: 0,
24
+ tests_failed: 0,
25
+ manual_review_required: true
26
+ },
27
+ evidence: {
28
+ commands: [],
29
+ files: [],
30
+ db_safety: null,
31
+ codex_app: null,
32
+ computer_use: null,
33
+ image_voxels: null,
34
+ triwiki: null
35
+ },
36
+ claims: [],
37
+ unverified: [],
38
+ blockers: [],
39
+ next_human_actions: [],
40
+ ...overrides
41
+ };
42
+ }
@@ -0,0 +1,81 @@
1
+ import path from 'node:path';
2
+ import { appendJsonl, ensureDir, nowIso, packageRoot, writeJsonAtomic, writeTextAtomic } from '../fsx.mjs';
3
+ import { redactSecrets } from '../secret-redaction.mjs';
4
+ import { emptyCompletionProof } from './proof-schema.mjs';
5
+ import { validateCompletionProof } from './validation.mjs';
6
+
7
+ export function proofDir(root = packageRoot()) {
8
+ return path.join(root, '.sneakoscope', 'proof');
9
+ }
10
+
11
+ export async function writeCompletionProof(root = packageRoot(), input = {}, opts = {}) {
12
+ const proof = redactSecrets(emptyCompletionProof({
13
+ generated_at: nowIso(),
14
+ ...input
15
+ }));
16
+ const validation = validateCompletionProof(proof);
17
+ const dir = proofDir(root);
18
+ await ensureDir(dir);
19
+ const latestJson = path.join(dir, 'latest.json');
20
+ const latestMd = path.join(dir, 'latest.md');
21
+ await writeJsonAtomic(latestJson, proof);
22
+ await writeTextAtomic(latestMd, renderProofMarkdown(proof, validation));
23
+ await writeJsonAtomic(path.join(dir, 'file-changes.json'), proof.evidence?.files || []);
24
+ await writeTextAtomic(path.join(dir, 'unverified.md'), renderUnverifiedMarkdown(proof));
25
+ if (opts.command) await appendJsonl(path.join(dir, 'commands.jsonl'), redactSecrets({ ts: nowIso(), ...opts.command }));
26
+ if (proof.mission_id) {
27
+ const missionDir = path.join(root, '.sneakoscope', 'missions', proof.mission_id);
28
+ await writeJsonAtomic(path.join(missionDir, 'completion-proof.json'), proof);
29
+ await writeTextAtomic(path.join(missionDir, 'completion-proof.md'), renderProofMarkdown(proof, validation));
30
+ }
31
+ return { ok: validation.ok, proof, validation, files: { latest_json: latestJson, latest_md: latestMd } };
32
+ }
33
+
34
+ export function renderProofMarkdown(proof = {}, validation = validateCompletionProof(proof)) {
35
+ const lines = [
36
+ '# SKS Completion Proof',
37
+ '',
38
+ `- Schema: ${proof.schema || 'unknown'}`,
39
+ `- Version: ${proof.version || 'unknown'}`,
40
+ `- Mission: ${proof.mission_id || 'latest-or-null'}`,
41
+ `- Route: ${proof.route || 'unknown'}`,
42
+ `- Status: ${proof.status || 'not_verified'}`,
43
+ `- Validation: ${validation.ok ? 'pass' : 'fail'}`,
44
+ '',
45
+ '## Summary',
46
+ '',
47
+ `- Files changed: ${proof.summary?.files_changed ?? 0}`,
48
+ `- Commands run: ${proof.summary?.commands_run ?? 0}`,
49
+ `- Tests passed: ${proof.summary?.tests_passed ?? 0}`,
50
+ `- Tests failed: ${proof.summary?.tests_failed ?? 0}`,
51
+ `- Manual review required: ${proof.summary?.manual_review_required === false ? 'false' : 'true'}`,
52
+ '',
53
+ '## Evidence',
54
+ '',
55
+ `- Commands: ${proof.evidence?.commands?.length || 0}`,
56
+ `- Files: ${proof.evidence?.files?.length || 0}`,
57
+ `- Image voxels: ${proof.evidence?.image_voxels?.anchors || proof.evidence?.image_voxels?.anchor_count || 0}`,
58
+ `- TriWiki: ${proof.evidence?.triwiki?.status || 'not_recorded'}`,
59
+ '',
60
+ '## Unverified',
61
+ ''
62
+ ];
63
+ const unverified = proof.unverified?.length ? proof.unverified : ['No unverified claims recorded.'];
64
+ for (const item of unverified) lines.push(`- ${typeof item === 'string' ? item : JSON.stringify(item)}`);
65
+ if (proof.blockers?.length) {
66
+ lines.push('', '## Blockers', '');
67
+ for (const blocker of proof.blockers) lines.push(`- ${typeof blocker === 'string' ? blocker : JSON.stringify(blocker)}`);
68
+ }
69
+ if (validation.issues?.length) {
70
+ lines.push('', '## Validation Issues', '');
71
+ for (const issue of validation.issues) lines.push(`- ${issue}`);
72
+ }
73
+ return `${lines.join('\n')}\n`;
74
+ }
75
+
76
+ function renderUnverifiedMarkdown(proof = {}) {
77
+ const lines = ['# SKS Unverified Claims', ''];
78
+ const items = proof.unverified?.length ? proof.unverified : ['No unverified claims recorded.'];
79
+ for (const item of items) lines.push(`- ${typeof item === 'string' ? item : JSON.stringify(item)}`);
80
+ return `${lines.join('\n')}\n`;
81
+ }
@@ -0,0 +1,74 @@
1
+ import path from 'node:path';
2
+ import { collectProofEvidence } from './evidence-collector.mjs';
3
+ import { writeCompletionProof } from './proof-writer.mjs';
4
+ import { normalizeProofRoute, routeRequiresImageVoxelAnchors } from './route-proof-policy.mjs';
5
+
6
+ export async function writeRouteCompletionProof(root, {
7
+ missionId = null,
8
+ route = null,
9
+ status = 'verified_partial',
10
+ gate = null,
11
+ summary = {},
12
+ artifacts = [],
13
+ evidence = {},
14
+ claims = [],
15
+ unverified = [],
16
+ blockers = [],
17
+ nextHumanActions = []
18
+ } = {}) {
19
+ const collected = await collectProofEvidence(root);
20
+ const normalizedRoute = normalizeProofRoute(route);
21
+ const mergedEvidence = {
22
+ ...collected,
23
+ ...evidence,
24
+ route_gate: gate || evidence.route_gate || null,
25
+ artifacts: normalizeArtifacts(root, artifacts)
26
+ };
27
+ const normalizedStatus = normalizeRouteProofStatus(status, {
28
+ route: normalizedRoute,
29
+ evidence: mergedEvidence,
30
+ blockers,
31
+ unverified
32
+ });
33
+ return writeCompletionProof(root, {
34
+ mission_id: missionId,
35
+ route: normalizedRoute,
36
+ status: normalizedStatus,
37
+ summary: {
38
+ files_changed: collected.files?.length || 0,
39
+ commands_run: mergedEvidence.commands?.length || 0,
40
+ tests_passed: 0,
41
+ tests_failed: 0,
42
+ manual_review_required: normalizedStatus !== 'verified',
43
+ ...summary
44
+ },
45
+ evidence: mergedEvidence,
46
+ claims,
47
+ unverified,
48
+ blockers,
49
+ next_human_actions: nextHumanActions
50
+ }, {
51
+ command: {
52
+ cmd: `sks proof route ${missionId || 'latest'}`,
53
+ route: normalizedRoute,
54
+ status: normalizedStatus
55
+ }
56
+ });
57
+ }
58
+
59
+ function normalizeRouteProofStatus(status, { route, evidence, blockers, unverified }) {
60
+ if (blockers?.length) return status === 'failed' ? 'failed' : 'blocked';
61
+ if (status === 'verified' && unverified?.length) return 'verified_partial';
62
+ if (routeRequiresImageVoxelAnchors(route)) {
63
+ const anchors = evidence?.image_voxels?.anchors ?? evidence?.image_voxels?.anchor_count ?? 0;
64
+ if (Number(anchors) <= 0) return status === 'verified' ? 'blocked' : status;
65
+ }
66
+ return status;
67
+ }
68
+
69
+ function normalizeArtifacts(root, artifacts = []) {
70
+ return artifacts.map((artifact) => {
71
+ if (typeof artifact !== 'string') return artifact;
72
+ return path.isAbsolute(artifact) ? path.relative(root, artifact).split(path.sep).join('/') : artifact;
73
+ });
74
+ }
@@ -0,0 +1,33 @@
1
+ import { containsPlaintextSecret } from '../secret-redaction.mjs';
2
+ import { readRouteProof } from './proof-reader.mjs';
3
+ import { validateCompletionProof } from './validation.mjs';
4
+ import { proofStatusBlocks, routeRequiresCompletionProof, routeRequiresImageVoxelAnchors } from './route-proof-policy.mjs';
5
+
6
+ export async function validateRouteCompletionProof(root, { missionId = null, route = null, state = {}, visualClaim = true } = {}) {
7
+ const proofRequired = state.proof_required === true || routeRequiresCompletionProof(route);
8
+ if (!proofRequired) return { ok: true, required: false, status: 'not_required', issues: [] };
9
+ const proof = await readRouteProof(root, missionId);
10
+ if (!proof) {
11
+ return {
12
+ ok: false,
13
+ required: true,
14
+ status: 'blocked',
15
+ issues: ['completion_proof_missing']
16
+ };
17
+ }
18
+ const validation = validateCompletionProof(proof);
19
+ const issues = [...validation.issues];
20
+ if (proofStatusBlocks(proof.status)) issues.push(`proof_status_${proof.status}`);
21
+ if (containsPlaintextSecret(proof)) issues.push('plaintext_secret');
22
+ if (routeRequiresImageVoxelAnchors(route || proof.route, { visualClaim })) {
23
+ const anchors = proof.evidence?.image_voxels?.anchors ?? proof.evidence?.image_voxels?.anchor_count ?? 0;
24
+ if (Number(anchors) <= 0) issues.push('image_voxel_anchors_missing');
25
+ }
26
+ return {
27
+ ok: issues.length === 0,
28
+ required: true,
29
+ status: issues.length ? 'blocked' : proof.status,
30
+ issues,
31
+ proof
32
+ };
33
+ }
@@ -0,0 +1,96 @@
1
+ export const SERIOUS_ROUTE_ALIASES = Object.freeze([
2
+ '$Team',
3
+ '$DFix',
4
+ '$QA-LOOP',
5
+ '$Research',
6
+ '$AutoResearch',
7
+ '$PPT',
8
+ '$Image-UX-Review',
9
+ '$UX-Review',
10
+ '$Visual-Review',
11
+ '$UI-UX-Review',
12
+ '$From-Chat-IMG',
13
+ '$Computer-Use',
14
+ '$CU',
15
+ '$DB',
16
+ '$Wiki',
17
+ '$GX',
18
+ '$Goal',
19
+ '$MAD-SKS',
20
+ 'hproof',
21
+ 'proof-field',
22
+ 'recallpulse'
23
+ ]);
24
+
25
+ export const VISUAL_ROUTE_ALIASES = Object.freeze([
26
+ '$Image-UX-Review',
27
+ '$UX-Review',
28
+ '$Visual-Review',
29
+ '$UI-UX-Review',
30
+ '$From-Chat-IMG',
31
+ '$PPT',
32
+ '$QA-LOOP',
33
+ '$Computer-Use',
34
+ '$CU',
35
+ '$GX'
36
+ ]);
37
+
38
+ const ROUTE_NORMALIZATION = Object.freeze({
39
+ team: '$Team',
40
+ dfix: '$DFix',
41
+ qaloop: '$QA-LOOP',
42
+ 'qa-loop': '$QA-LOOP',
43
+ research: '$Research',
44
+ autoresearch: '$AutoResearch',
45
+ ppt: '$PPT',
46
+ imageuxreview: '$Image-UX-Review',
47
+ 'image-ux-review': '$Image-UX-Review',
48
+ uxreview: '$UX-Review',
49
+ 'ux-review': '$UX-Review',
50
+ visualreview: '$Visual-Review',
51
+ 'visual-review': '$Visual-Review',
52
+ uiuxreview: '$UI-UX-Review',
53
+ 'ui-ux-review': '$UI-UX-Review',
54
+ fromchatimg: '$From-Chat-IMG',
55
+ 'from-chat-img': '$From-Chat-IMG',
56
+ computeruse: '$Computer-Use',
57
+ 'computer-use': '$Computer-Use',
58
+ cu: '$CU',
59
+ db: '$DB',
60
+ wiki: '$Wiki',
61
+ gx: '$GX',
62
+ goal: '$Goal',
63
+ madsks: '$MAD-SKS',
64
+ 'mad-sks': '$MAD-SKS',
65
+ hproof: 'hproof',
66
+ prooffield: 'proof-field',
67
+ 'proof-field': 'proof-field',
68
+ recallpulse: 'recallpulse'
69
+ });
70
+
71
+ export function normalizeProofRoute(route) {
72
+ const raw = String(route || '').trim();
73
+ if (!raw) return null;
74
+ if (SERIOUS_ROUTE_ALIASES.includes(raw) || VISUAL_ROUTE_ALIASES.includes(raw)) return raw;
75
+ const stripped = raw.replace(/^\$/, '').replace(/[^A-Za-z0-9-]+/g, '').toLowerCase();
76
+ return ROUTE_NORMALIZATION[stripped] || raw;
77
+ }
78
+
79
+ export function routeRequiresCompletionProof(route) {
80
+ const normalized = normalizeProofRoute(route);
81
+ return SERIOUS_ROUTE_ALIASES.includes(normalized);
82
+ }
83
+
84
+ export function routeRequiresImageVoxelAnchors(route, opts = {}) {
85
+ const normalized = normalizeProofRoute(route);
86
+ if (opts.visualClaim === false) return false;
87
+ return VISUAL_ROUTE_ALIASES.includes(normalized);
88
+ }
89
+
90
+ export function routeFromState(state = {}) {
91
+ return normalizeProofRoute(state.route_command || state.route || state.mode || state.route_id || state.id);
92
+ }
93
+
94
+ export function proofStatusBlocks(status) {
95
+ return status === 'failed' || status === 'blocked' || status === 'not_verified';
96
+ }