sneakoscope 7.3.0 → 7.5.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.
Files changed (151) hide show
  1. package/README.md +117 -5
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +2 -1
  6. package/dist/cli/install-helpers-codex-lb-selftest.js +2 -1
  7. package/dist/cli/install-helpers-install-support.js +6 -16
  8. package/dist/cli/install-helpers.js +59 -50
  9. package/dist/cli/router.js +9 -1
  10. package/dist/config/skills-manifest.json +50 -71
  11. package/dist/core/codex-app.js +5 -5
  12. package/dist/core/codex-native/core-skill-manifest.js +9 -6
  13. package/dist/core/commands/image-ux-review-command.js +243 -50
  14. package/dist/core/commands/naruto-command.js +245 -8
  15. package/dist/core/commands/ppt-command.js +2 -2
  16. package/dist/core/commands/telegram-command.js +123 -10
  17. package/dist/core/commands/triwiki-command.js +11 -1
  18. package/dist/core/commands/triwiki-graph-command.js +160 -0
  19. package/dist/core/commands/wiki-command.js +100 -20
  20. package/dist/core/errors/message.js +3 -0
  21. package/dist/core/feature-registry.js +2 -2
  22. package/dist/core/fsx.js +7 -3
  23. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  24. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  25. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  26. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  27. package/dist/core/hooks-runtime.js +28 -86
  28. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  29. package/dist/core/image-ux-review/imagegen-adapter.js +54 -18
  30. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  31. package/dist/core/image-ux-review.js +182 -7
  32. package/dist/core/imagegen/codex-lb-imagegen-target.js +22 -7
  33. package/dist/core/imagegen/imagegen-capability.js +11 -4
  34. package/dist/core/imagegen/require-imagegen.js +29 -5
  35. package/dist/core/init/skills.js +22 -10
  36. package/dist/core/install/installed-package-smoke.js +157 -4
  37. package/dist/core/managed-path-safety.js +8 -10
  38. package/dist/core/mcp-config/backup.js +2 -4
  39. package/dist/core/mcp-config/config-reader.js +2 -4
  40. package/dist/core/mcp-config/scope.js +3 -5
  41. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  42. package/dist/core/naruto/context-graph-advisor.js +0 -0
  43. package/dist/core/pipeline-internals/runtime-core.js +44 -0
  44. package/dist/core/policy/latest-version-guidance.js +113 -0
  45. package/dist/core/ppt.js +8 -8
  46. package/dist/core/release/gate-manifest.js +32 -1
  47. package/dist/core/release/npm-stage-contract.js +21 -0
  48. package/dist/core/release/package-size-budget.js +2 -2
  49. package/dist/core/release/release-gate-contract.js +5 -0
  50. package/dist/core/release/stage-publish.js +27 -0
  51. package/dist/core/responses-stream.js +65 -19
  52. package/dist/core/routes/design-policy.js +7 -2
  53. package/dist/core/routes/dollar-manifest-lite.js +0 -4
  54. package/dist/core/routes/evidence.js +1 -1
  55. package/dist/core/routes.js +12 -8
  56. package/dist/core/safety/command-contract/contracts.js +2 -0
  57. package/dist/core/safety/command-contract/types.js +1 -1
  58. package/dist/core/search/context-graph-seeds.js +205 -0
  59. package/dist/core/search/context.js +0 -0
  60. package/dist/core/search/provider.js +2 -2
  61. package/dist/core/subagents/naruto-help-contract.js +2 -1
  62. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  63. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  64. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  65. package/dist/core/subagents/official-subagent-runner.js +26 -8
  66. package/dist/core/subagents/triwiki-attention.js +135 -123
  67. package/dist/core/telegram/bot-api.js +59 -4
  68. package/dist/core/telegram/config.js +3 -2
  69. package/dist/core/telegram/hub.js +3 -2
  70. package/dist/core/telegram/owner-lock.js +4 -6
  71. package/dist/core/telegram/setup.js +223 -73
  72. package/dist/core/triwiki/code-pack.js +12 -120
  73. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  74. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  75. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  76. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  77. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  78. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  79. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  80. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  81. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  82. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  83. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  84. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  85. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  86. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  87. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  88. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  89. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  90. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  91. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  92. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  93. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  94. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  95. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  96. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  97. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  98. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  99. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  100. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  101. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  102. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  103. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  104. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  105. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  106. package/dist/core/triwiki/context-graph/ids.js +0 -0
  107. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  108. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  109. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  110. package/dist/core/triwiki/context-graph/paths.js +116 -0
  111. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  112. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  113. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  114. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  115. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  116. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  117. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  118. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  119. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  120. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  121. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  122. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  123. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  124. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  125. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  126. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  127. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  128. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  129. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  130. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  131. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  132. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  133. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  134. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  135. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  136. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  137. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  138. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  139. package/dist/core/verification/context-graph-affected.js +271 -0
  140. package/dist/core/version.js +1 -1
  141. package/dist/scripts/context-graph-check.js +191 -0
  142. package/dist/scripts/helper-dedup-check.js +0 -1
  143. package/dist/scripts/installed-package-smoke-check.js +4 -1
  144. package/dist/scripts/latest-version-guidance-check.js +74 -0
  145. package/dist/scripts/release-metadata-check.js +20 -3
  146. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  147. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  148. package/package.json +5 -3
  149. package/release-gates.v2.json +174 -0
  150. package/schemas/triwiki/context-graph.schema.json +119 -0
  151. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { projectRoot, readJson, writeJsonAtomic, appendJsonl, nowIso, runProcess, sha256, packageRoot, tmpdir } from './fsx.js';
2
+ import { projectRoot, readJson, writeJsonAtomic, appendJsonl, nowIso, sha256, packageRoot } from './fsx.js';
3
3
  import { looksInteractiveCommand, interactiveCommandReason } from './no-question-guard.js';
4
4
  import { loadStateForSession, missionDir, sessionStateKey, setCurrent, stateFileForSession } from './mission.js';
5
5
  import { checkDbOperation, dbBlockReason, handleMadSksUserConfirmation } from './db-safety.js';
@@ -42,6 +42,7 @@ import { activeAuthoritativeSksSkillRefresh, activeGoalOverlayContext, attachAut
42
42
  import { sealedSubagentRoutingContext, subagentRouteContext } from './hooks-runtime/subagent-context.js';
43
43
  export { loadHookPayload, normalizeHookResult };
44
44
  export { refreshOfficialSubagentCompletionArtifacts };
45
+ export { selftestCodexCommitHooks } from './hooks-runtime/codex-commit-hooks-selftest.js';
45
46
  async function loadState(root, payload = {}) {
46
47
  const sessionKey = conversationId(payload);
47
48
  if (!explicitConversationId(payload))
@@ -863,8 +864,9 @@ async function hookStop(root, state, payload, noQuestion, sessionKey = null) {
863
864
  await refreshOfficialSubagentCompletionArtifacts(root, state, last, sessionKey).catch(() => null);
864
865
  }
865
866
  const routeDecision = await evaluateStop(root, state, payload, { noQuestion });
866
- if (routeDecision)
867
+ if (routeDecision && !successfulAppNarutoStopNeedsVisibleSummary(state, routeDecision)) {
867
868
  return routeDecision;
869
+ }
868
870
  if (!noQuestion) {
869
871
  const languageBasis = state?.prompt || state?.task || extractUserPrompt(payload) || last;
870
872
  if (!hasHonestMode(last)) {
@@ -892,13 +894,23 @@ async function hookStop(root, state, payload, noQuestion, sessionKey = null) {
892
894
  }
893
895
  if (state?.honest_loop_required)
894
896
  await resolveHonestModeLoopback(root, state, sessionKey);
895
- return { continue: true };
897
+ return routeDecision || { continue: true };
896
898
  }
897
899
  return {
898
900
  decision: 'block',
899
901
  reason: 'SKS no-question run is not done. Continue autonomously, fix failing checks, update the active gate file, and do not ask the user.'
900
902
  };
901
903
  }
904
+ function successfulAppNarutoStopNeedsVisibleSummary(state = {}, routeDecision = {}) {
905
+ const route = String(state?.route || state?.route_command || state?.mode || '')
906
+ .replace(/^\$/, '')
907
+ .replace(/[-_]/g, '')
908
+ .toUpperCase();
909
+ return route === 'NARUTO'
910
+ && Boolean(state?.session_scope)
911
+ && routeDecision?.continue === true
912
+ && !['hard_blocked', 'route_closed'].includes(String(routeDecision?.action || ''));
913
+ }
902
914
  async function consumeLightRouteStop(root, payload = {}) {
903
915
  const file = path.join(root, '.sneakoscope', 'state', LIGHT_ROUTE_STOP_ARTIFACT);
904
916
  const record = await readJson(file, null).catch(() => null);
@@ -982,12 +994,22 @@ function hasHonestModeUnresolvedGap(text) {
982
994
  }
983
995
  export function honestModeGapLines(text) {
984
996
  const issue = /(gap|remaining|unverified|not verified|not run|not complete|incomplete|failed|blocked|blocker|could not|couldn't|missing|미완료|미검증|미실행|실패|차단|누락|못했|못 했|안 했|안함|아직|남은)/i;
985
- return String(text || '')
997
+ const lines = String(text || '')
986
998
  .split(/\n/)
987
- .map((line) => line.trim())
988
- .filter((line) => issue.test(line) && !honestGapLineResolved(line))
999
+ .map((line) => line.trim());
1000
+ return lines
1001
+ .filter((line, index) => issue.test(line)
1002
+ && !honestGapLineResolved(line)
1003
+ && !honestGapHeadingResolved(line, lines.slice(index + 1).find((candidate) => candidate.length > 0)))
989
1004
  .slice(0, 12);
990
1005
  }
1006
+ function honestGapHeadingResolved(line, nextLine) {
1007
+ const heading = String(line || '').replace(/^#{1,6}\s*/, '').replace(/\s*[::]\s*$/, '').trim();
1008
+ if (!/^(?:remaining|unresolved)\s+(?:gaps?|issues?)$|^(?:남은|미해결)\s*(?:문제|항목|갭|gap)$/i.test(heading)) {
1009
+ return false;
1010
+ }
1011
+ return /^(?:없음|없습니다|없다|none|no(?:ne)?|0|0개)[.!。]?$/i.test(String(nextLine || '').trim());
1012
+ }
991
1013
  function honestGapLineResolved(line) {
992
1014
  if (/(?:unverified|미검증)\s*:\s*\[\s*\]/i.test(line) && /blockers?\s*:\s*\[\s*\]/i.test(line))
993
1015
  return true;
@@ -1080,83 +1102,3 @@ export async function emitHook(name) {
1080
1102
  const result = await hookMain(name);
1081
1103
  process.stdout.write(`${JSON.stringify(normalizeHookResult(name, result))}\n`);
1082
1104
  }
1083
- export async function selftestCodexCommitHooks() {
1084
- const root = tmpdir();
1085
- const hookBin = path.join(packageRoot(), 'dist', 'bin', 'sks.js');
1086
- const env = { SKS_DISABLE_UPDATE_CHECK: '1' };
1087
- const setup = await runProcess(process.execPath, [hookBin, 'setup', '--install-scope', 'project'], { cwd: root, env, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
1088
- if (setup.code !== 0)
1089
- throw new Error(`selftest failed: commit setup ${setup.code}: ${setup.stderr}`);
1090
- const runHook = (name, payload) => runProcess(process.execPath, [hookBin, 'hook', name], { cwd: root, input: JSON.stringify({ cwd: root, ...payload }), env, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
1091
- const id = 'commit-selftest';
1092
- const hook = await runHook('user-prompt-submit', { conversation_id: id, action: 'codex_git_commit', prompt: 'Generate a git commit message for the staged diff.' });
1093
- if (hook.code !== 0)
1094
- throw new Error(`selftest failed: commit hook ${hook.code}: ${hook.stderr}`);
1095
- const hookJson = JSON.parse(hook.stdout);
1096
- if (hookJson.decision === 'block' || hookJson.hookSpecificOutput?.additionalContext || !String(hookJson.systemMessage || '').includes('git action'))
1097
- throw new Error('selftest failed: commit route bypass');
1098
- const stop = await runHook('stop', { conversation_id: id, last_assistant_message: 'Fix Codex App commit message hook bypass' });
1099
- if (stop.code !== 0)
1100
- throw new Error(`selftest failed: commit stop ${stop.code}: ${stop.stderr}`);
1101
- const stopJson = JSON.parse(stop.stdout);
1102
- if (stopJson.decision === 'block' || !String(stopJson.systemMessage || '').includes('accepted without route finalization'))
1103
- throw new Error('selftest failed: commit stop bypass');
1104
- const commitPushId = 'commit-push-selftest';
1105
- const appCommitPushHook = await runHook('user-prompt-submit', { conversation_id: commitPushId, action: 'Codex App Git Actions Commit and Push', prompt: 'Commit and push changes.' });
1106
- if (appCommitPushHook.code !== 0)
1107
- throw new Error(`selftest failed: app commit-push hook ${appCommitPushHook.code}: ${appCommitPushHook.stderr}`);
1108
- const appCommitPushJson = JSON.parse(appCommitPushHook.stdout);
1109
- if (appCommitPushJson.decision === 'block' || appCommitPushJson.hookSpecificOutput?.additionalContext || !String(appCommitPushJson.systemMessage || '').includes('git action'))
1110
- throw new Error('selftest failed: app commit-push route bypass');
1111
- const appCommitPushStop = await runHook('stop', { conversation_id: commitPushId, last_assistant_message: 'Commit and push complete.' });
1112
- if (appCommitPushStop.code !== 0)
1113
- throw new Error(`selftest failed: app commit-push stop ${appCommitPushStop.code}: ${appCommitPushStop.stderr}`);
1114
- if (JSON.parse(appCommitPushStop.stdout).decision === 'block')
1115
- throw new Error('selftest failed: app commit-push stop bypass');
1116
- const appPushId = 'app-push-selftest';
1117
- const appPushHook = await runHook('user-prompt-submit', { conversation_id: appPushId, metadata: { source: 'codex_app', action: 'Git Actions Push' }, prompt: 'Push changes.' });
1118
- if (appPushHook.code !== 0)
1119
- throw new Error(`selftest failed: app push hook ${appPushHook.code}: ${appPushHook.stderr}`);
1120
- const appPushJson = JSON.parse(appPushHook.stdout);
1121
- if (appPushJson.decision === 'block' || appPushJson.hookSpecificOutput?.additionalContext || !String(appPushJson.systemMessage || '').includes('git action'))
1122
- throw new Error('selftest failed: app push metadata route bypass');
1123
- const appPushStop = await runHook('stop', { conversation_id: appPushId, metadata: { source: 'codex_app', action: 'Git Actions Push' }, last_assistant_message: 'Done.' });
1124
- if (appPushStop.code !== 0)
1125
- throw new Error(`selftest failed: app push stop ${appPushStop.code}: ${appPushStop.stderr}`);
1126
- if (JSON.parse(appPushStop.stdout).decision === 'block')
1127
- throw new Error('selftest failed: app push metadata stop bypass');
1128
- const metadataLightId = 'metadata-light-commit-push-selftest';
1129
- const metadataLightHook = await runHook('user-prompt-submit', { conversation_id: metadataLightId, prompt: 'Commit and push changes.' });
1130
- if (metadataLightHook.code !== 0)
1131
- throw new Error(`selftest failed: metadata-light commit-push hook ${metadataLightHook.code}: ${metadataLightHook.stderr}`);
1132
- const metadataLightJson = JSON.parse(metadataLightHook.stdout);
1133
- if (metadataLightJson.decision === 'block' || metadataLightJson.hookSpecificOutput?.additionalContext || !String(metadataLightJson.systemMessage || '').includes('git action'))
1134
- throw new Error('selftest failed: metadata-light app commit-push route bypass');
1135
- const metadataLightStop = await runHook('stop', { conversation_id: metadataLightId, last_assistant_message: 'Commit and push complete.' });
1136
- if (metadataLightStop.code !== 0)
1137
- throw new Error(`selftest failed: metadata-light commit-push stop ${metadataLightStop.code}: ${metadataLightStop.stderr}`);
1138
- if (JSON.parse(metadataLightStop.stdout).decision === 'block')
1139
- throw new Error('selftest failed: metadata-light commit-push stop bypass');
1140
- const settingsHook = await runHook('user-prompt-submit', { model: 'future-codex-catalog-model', metadata: { source: 'codex_app_settings', feature: 'speed profile' } });
1141
- if (settingsHook.code !== 0)
1142
- throw new Error(`selftest failed: settings hook ${settingsHook.code}: ${settingsHook.stderr}`);
1143
- const settingsJson = JSON.parse(settingsHook.stdout);
1144
- if (settingsJson.decision === 'block' || settingsJson.hookSpecificOutput?.additionalContext || !String(settingsJson.systemMessage || '').includes('settings/profile event ignored'))
1145
- throw new Error('selftest failed: settings/profile event should not route or block');
1146
- const userHook = await runHook('user-prompt-submit', { prompt: '[커밋 메시지를 생성하지 못했습니다.] 코덱스 앱에서 이 버그 수정해줘' });
1147
- if (userHook.code !== 0)
1148
- throw new Error(`selftest failed: user commit hook ${userHook.code}: ${userHook.stderr}`);
1149
- if (!JSON.parse(userHook.stdout).hookSpecificOutput?.additionalContext?.includes('$Naruto route prepared'))
1150
- throw new Error('selftest failed: user prompt route');
1151
- const userCommitPushHook = await runHook('user-prompt-submit', { prompt: '배포하게 커밋하고 푸쉬해줘' });
1152
- if (userCommitPushHook.code !== 0)
1153
- throw new Error(`selftest failed: user commit-push hook ${userCommitPushHook.code}: ${userCommitPushHook.stderr}`);
1154
- const userCommitPushJson = JSON.parse(userCommitPushHook.stdout);
1155
- if (userCommitPushJson.decision === 'block' || userCommitPushJson.hookSpecificOutput?.additionalContext || !String(userCommitPushJson.systemMessage || '').includes('git action'))
1156
- throw new Error('selftest failed: user commit-push prompt should bypass route');
1157
- const userCommitPushStop = await runHook('stop', { last_assistant_message: 'Commit and push complete.' });
1158
- if (userCommitPushStop.code !== 0)
1159
- throw new Error(`selftest failed: user commit-push stop ${userCommitPushStop.code}: ${userCommitPushStop.stderr}`);
1160
- if (JSON.parse(userCommitPushStop.stdout).decision === 'block')
1161
- throw new Error('selftest failed: user commit-push stop bypass');
1162
- }
@@ -27,12 +27,18 @@ export function buildIssueLedgerFromGeneratedCallouts(generatedReviewLedger = {}
27
27
  blockers.push('ux_review_text_only_fallback');
28
28
  if (generated.length && !issues.length)
29
29
  blockers.push(generated.some((image) => image.callout_extraction_status === 'pending') ? 'callout_extraction_pending' : 'callout_extraction_schema_failed');
30
+ const primaryGenerated = generated.find((image) => image.id === existing?.generated_image_id)
31
+ || generated.find((image) => image.id === issues[0]?.generated_review_image_id)
32
+ || generated[0]
33
+ || null;
30
34
  return {
31
35
  schema: 'sks.image-ux-issue-ledger.v3',
32
36
  schema_version: 3,
33
37
  extraction_source: 'image-ux-generated-review-ledger.json',
34
38
  extraction_method: 'codex_exec_resume_output_schema_preferred',
35
39
  extraction_schema: 'schemas/codex/image-ux-issue-ledger.schema.json',
40
+ generated_image_id: primaryGenerated?.id || existing?.generated_image_id || issues[0]?.generated_review_image_id || null,
41
+ generated_image_sha256: primaryGenerated?.sha256 || existing?.generated_image_sha256 || issues[0]?.generated_image_sha256 || null,
36
42
  extracted_from_generated_callout: issues.length > 0,
37
43
  issues,
38
44
  blocking_issue_count: issues.filter((issue) => ['P0', 'P1'].includes(issue.severity) && !['fixed', 'accepted_not_applicable'].includes(issue.status)).length,
@@ -12,9 +12,11 @@ import { discoverCodexAppGeneratedImage } from './codex-app-generated-image-disc
12
12
  import { writeImageArtifactPathContract } from '../image/image-artifact-path-contract.js';
13
13
  import { registerImageArtifact } from '../image/image-artifact-registry.js';
14
14
  const DEFAULT_OPENAI_IMAGE_EDITS_ENDPOINT = 'https://api.openai.com/v1/images/edits';
15
+ export const DEFAULT_IMAGEGEN_FETCH_TIMEOUT_MS = 180000;
16
+ const responseDeadlines = new WeakMap();
15
17
  export function buildCalloutPrompt(sourceScreenId, context = {}) {
16
18
  return [
17
- 'Review this UI screenshot as a senior product design lead.',
19
+ 'Review this UI screenshot as a senior Toss UI/UX product designer.',
18
20
  `Screenshot source id: ${sourceScreenId}.`,
19
21
  'Output must be a new image artifact, not prose.',
20
22
  'Text-only response is invalid.',
@@ -373,7 +375,7 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
373
375
  status: 'blocked',
374
376
  blocker: auth.blocker,
375
377
  setup_guidance: auth.auth_source === 'CODEX_LB_API_KEY'
376
- ? 'CODEX_LB_API_KEY is only a non-Codex API fallback when explicitly enabled; it does not satisfy Codex App $imagegen evidence. Attach a real Codex App $imagegen output image for full SKS verification.'
378
+ ? 'Repair the selected codex-lb credential binding. Completed output from a selected provider can satisfy full evidence; codex-lb used only as a fallback while another provider is selected cannot.'
377
379
  : 'Set OPENAI_API_KEY only for an explicit non-Codex Images API fallback, or attach a real Codex App $imagegen output image for full SKS verification.',
378
380
  local_only: true
379
381
  };
@@ -463,6 +465,8 @@ export function createOpenAIImagesApiAdapter(opts = {}) {
463
465
  output_id: meta.output_id,
464
466
  output_source: responsesOutputSource,
465
467
  image_output_recovered_from_stream: payload?.image_output_recovered_from_stream === true,
468
+ image_output_provenance: payload?.image_output_provenance || 'response.output',
469
+ image_output_partial_frame: false,
466
470
  image_artifact_path_contract: imageContract?.artifact_path || null,
467
471
  dimensions: { width: meta.width, height: meta.height, format: meta.format },
468
472
  latency_ms: Date.now() - started,
@@ -605,6 +609,8 @@ function codexLbTargetInputs(source = {}) {
605
609
  const inputs = {};
606
610
  if (source?.home)
607
611
  inputs.home = source.home;
612
+ if (source?.codexHome)
613
+ inputs.codexHome = source.codexHome;
608
614
  if (source?.env)
609
615
  inputs.env = source.env;
610
616
  if (typeof source?.configText === 'string')
@@ -625,15 +631,22 @@ export function imagegenCapabilityBlocker(surface = 'Codex App $imagegen') {
625
631
  blocker: 'imagegen_capability_missing',
626
632
  surface,
627
633
  model: 'gpt-image-2',
628
- guidance: 'Run the request in Codex App with $imagegen/gpt-image-2 and attach the generated annotated review image path. OPENAI_API_KEY or CODEX_LB_API_KEY may be used only for an explicitly requested non-Codex API fallback, and that fallback does not satisfy Codex App imagegen evidence. SKS must not fabricate or substitute a text-only review.'
634
+ guidance: 'Run the request with gpt-image-2 through Codex App $imagegen or a selected, ready codex-lb provider and bind the completed generated image path. OPENAI_API_KEY, or codex-lb used while another provider is selected, remains non-Codex fallback evidence. SKS must not fabricate or substitute a text-only review.'
629
635
  };
630
636
  }
631
637
  async function resolveImagesApiAuth(opts = {}) {
638
+ const codexLb = opts.codexLb?.available === true ? opts.codexLb : null;
639
+ const target = opts.codexLbTarget || (codexLb
640
+ ? await resolveCodexLbImagegenTarget(codexLbTargetInputs(opts)).catch(() => null)
641
+ : null);
642
+ if (target?.selected === true)
643
+ return codexLbImagesApiAuth(opts, codexLb, target);
632
644
  const openAiKey = String(opts.apiKey || process.env.OPENAI_API_KEY || '').trim();
633
645
  if (openAiKey) {
634
646
  return {
635
647
  apiKey: openAiKey,
636
648
  auth_source: 'OPENAI_API_KEY',
649
+ api_key_source: null,
637
650
  responses_model: responsesImagegenModel(opts),
638
651
  responses_model_source: responsesImagegenModel(opts) ? 'explicit' : null,
639
652
  endpoint: imageEditsEndpoint(opts.baseUrl || 'https://api.openai.com/v1'),
@@ -641,11 +654,11 @@ async function resolveImagesApiAuth(opts = {}) {
641
654
  blocker: null
642
655
  };
643
656
  }
644
- const codexLb = opts.codexLb?.available === true ? opts.codexLb : null;
645
657
  if (!codexLb) {
646
658
  return {
647
659
  apiKey: null,
648
660
  auth_source: null,
661
+ api_key_source: null,
649
662
  responses_model: responsesImagegenModel(opts),
650
663
  responses_model_source: null,
651
664
  endpoint: imageEditsEndpoint(opts.baseUrl || 'https://api.openai.com/v1'),
@@ -653,10 +666,12 @@ async function resolveImagesApiAuth(opts = {}) {
653
666
  blocker: 'openai_api_key_missing'
654
667
  };
655
668
  }
656
- const envKey = codexLb.env_key || 'CODEX_LB_API_KEY';
657
- const target = opts.codexLbTarget || await resolveCodexLbImagegenTarget(codexLbTargetInputs(opts)).catch(() => null);
669
+ return codexLbImagesApiAuth(opts, codexLb, target);
670
+ }
671
+ function codexLbImagesApiAuth(opts, codexLb, target) {
672
+ const envKey = codexLb?.env_key || 'CODEX_LB_API_KEY';
658
673
  const codexLbKey = String(opts.codexLbApiKey || target?.api_key || '').trim();
659
- const baseUrl = target?.base_url || codexLb.base_url || opts.baseUrl || '';
674
+ const baseUrl = target?.base_url || codexLb?.base_url || opts.baseUrl || '';
660
675
  return {
661
676
  apiKey: codexLbKey || null,
662
677
  auth_source: envKey,
@@ -677,23 +692,27 @@ function responsesEndpoint(baseUrl = '') {
677
692
  return /\/responses$/i.test(base) ? base : `${base}/responses`;
678
693
  }
679
694
  function responsesImagegenModel(opts = {}) {
680
- return String(opts.responsesModel || process.env.SKS_IMAGEGEN_RESPONSES_MODEL || process.env.OPENAI_MODEL || '').trim();
695
+ return String(opts.responsesModel || process.env.SKS_IMAGEGEN_RESPONSES_MODEL || '').trim();
681
696
  }
682
697
  function imagegenFetchTimeoutMs(opts = {}) {
683
- const value = Number(opts.fetchTimeoutMs || process.env.SKS_IMAGEGEN_FETCH_TIMEOUT_MS || 90000);
684
- return Number.isFinite(value) && value > 0 ? value : 90000;
698
+ const value = Number(opts.fetchTimeoutMs || process.env.SKS_IMAGEGEN_FETCH_TIMEOUT_MS || DEFAULT_IMAGEGEN_FETCH_TIMEOUT_MS);
699
+ return Number.isFinite(value) && value > 0 ? value : DEFAULT_IMAGEGEN_FETCH_TIMEOUT_MS;
685
700
  }
686
701
  async function fetchWithTimeout(url, init, timeoutMs) {
687
702
  const controller = new AbortController();
703
+ const startedAt = Date.now();
688
704
  const timeout = setTimeout(() => controller.abort(new Error(`imagegen_fetch_timeout_${timeoutMs}ms`)), timeoutMs);
689
705
  try {
690
- return await fetch(url, { ...init, signal: controller.signal });
706
+ const response = await fetch(url, { ...init, signal: controller.signal });
707
+ responseDeadlines.set(response, { controller, startedAt, timeoutMs, timer: timeout });
708
+ return response;
691
709
  }
692
- finally {
710
+ catch (err) {
693
711
  clearTimeout(timeout);
712
+ throw err;
694
713
  }
695
714
  }
696
- async function readResponsePayload(response, timeoutMs = 90000) {
715
+ async function readResponsePayload(response, timeoutMs = DEFAULT_IMAGEGEN_FETCH_TIMEOUT_MS) {
697
716
  const text = await textWithTimeout(response, timeoutMs);
698
717
  if (!text)
699
718
  return {};
@@ -708,23 +727,39 @@ async function readResponsePayload(response, timeoutMs = 90000) {
708
727
  }
709
728
  }
710
729
  async function textWithTimeout(response, timeoutMs) {
730
+ const deadline = responseDeadlines.get(response);
731
+ if (deadline)
732
+ clearTimeout(deadline.timer);
733
+ const remainingMs = deadline
734
+ ? Math.max(1, deadline.timeoutMs - (Date.now() - deadline.startedAt))
735
+ : timeoutMs;
711
736
  let timeout = null;
712
737
  try {
713
738
  return await Promise.race([
714
739
  response.text(),
715
740
  new Promise((_, reject) => {
716
- timeout = setTimeout(() => reject(new Error(`imagegen_response_read_timeout_${timeoutMs}ms`)), timeoutMs);
741
+ timeout = setTimeout(() => {
742
+ const error = new Error(`imagegen_response_read_timeout_${timeoutMs}ms`);
743
+ deadline?.controller.abort(error);
744
+ void response.body?.cancel(error).catch(() => { });
745
+ reject(error);
746
+ }, remainingMs);
717
747
  })
718
748
  ]);
719
749
  }
720
750
  finally {
721
751
  if (timeout)
722
752
  clearTimeout(timeout);
753
+ responseDeadlines.delete(response);
723
754
  }
724
755
  }
725
756
  function findResponsesImageGenerationOutput(payload) {
757
+ if (payload?.image_output_partial_frame === true)
758
+ return null;
726
759
  for (const output of Array.isArray(payload?.output) ? payload.output : []) {
727
760
  if (String(output?.type || '') === 'image_generation_call') {
761
+ if (String(output?.status || '') === 'partial')
762
+ continue;
728
763
  const b64 = typeof output?.result === 'string' ? output.result : output?.result?.b64_json || output?.b64_json || null;
729
764
  if (b64)
730
765
  return { b64: String(b64), id: output?.id || payload?.id || null };
@@ -813,10 +848,8 @@ function imagegenRetryOptions(opts = {}) {
813
848
  return {
814
849
  sleep: opts.retrySleep,
815
850
  classifyError: (err) => {
816
- const code = String(err?.code
817
- || err?.name
818
- || err?.message
819
- || '');
851
+ const row = err;
852
+ const code = [row?.code, row?.name, row?.message].filter(Boolean).join(' ');
820
853
  if (/AbortError|timeout|abort/i.test(code))
821
854
  return { code: 'ETIMEDOUT', status: null };
822
855
  if (/ECONNRESET|EAI_AGAIN|ETIMEDOUT|ENOTFOUND|ECONNREFUSED|fetch failed/i.test(code))
@@ -834,6 +867,9 @@ function summarizeImagegenPayload(payload) {
834
867
  model: payload?.model || null,
835
868
  error_type: payload?.error?.type || null,
836
869
  error_code: payload?.error?.code || null,
870
+ image_output_partial_frame: payload?.image_output_partial_frame === true,
871
+ partial_image_output_present: payload?.partial_image_output_present === true,
872
+ image_output_provenance: payload?.image_output_provenance || null,
837
873
  output_count: outputs.length,
838
874
  output: outputs.slice(0, 8).map((output) => ({
839
875
  id: output?.id || null,
@@ -130,6 +130,7 @@ export function buildRealCalloutExtractionPrompt(input) {
130
130
  const source = input.sourceScreenshot || {};
131
131
  return [
132
132
  'Analyze the generated UX review image pixels directly.',
133
+ 'The source screenshot is context only; do not analyze it instead of the generated annotated image.',
133
134
  'Return only visible numbered callouts from the generated image.',
134
135
  'Do not invent issues, requirements, or invisible callouts.',
135
136
  'Return bbox coordinates in the generated image coordinate system as [x,y,width,height].',