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
@@ -4,9 +4,9 @@ import { ensureDir, nowIso, projectRoot, readJson, writeJsonAtomic } from '../fs
4
4
  import { createMission, findLatestMission, loadMission } from '../mission.js';
5
5
  import { flag, readOption } from './command-utils.js';
6
6
  import { printJson } from '../../cli/output.js';
7
- import { IMAGE_UX_REVIEW_FIX_LOOP_ARTIFACT, IMAGE_UX_REVIEW_FIX_TASK_PLAN_ARTIFACT, IMAGE_UX_REVIEW_CALLOUT_EXTRACTION_REPORT_ARTIFACT, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_GPT_IMAGE_2_RESPONSE_ARTIFACT, IMAGE_UX_REVIEW_GATE_ARTIFACT, IMAGE_UX_REVIEW_HONEST_MODE_ARTIFACT, IMAGE_UX_REVIEW_GPT_IMAGE_2_REQUEST_ARTIFACT, IMAGE_UX_REVIEW_IMAGEGEN_REQUEST_ARTIFACT, IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_POLICY_ARTIFACT, IMAGE_UX_REVIEW_RECAPTURE_ARTIFACT, IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT, imageUxReviewProofEvidence, buildImageUxCalloutExtractionReport, writeImageUxReviewRouteArtifacts } from '../image-ux-review.js';
7
+ import { IMAGE_UX_REVIEW_FIX_LOOP_ARTIFACT, IMAGE_UX_REVIEW_FIX_TASK_PLAN_ARTIFACT, IMAGE_UX_REVIEW_CALLOUT_EXTRACTION_REPORT_ARTIFACT, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_GPT_IMAGE_2_RESPONSE_ARTIFACT, IMAGE_UX_REVIEW_GATE_ARTIFACT, IMAGE_UX_REVIEW_HONEST_MODE_ARTIFACT, IMAGE_UX_REVIEW_GPT_IMAGE_2_REQUEST_ARTIFACT, IMAGE_UX_REVIEW_IMAGEGEN_REQUEST_ARTIFACT, IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_POLICY_ARTIFACT, IMAGE_UX_REVIEW_RECAPTURE_ARTIFACT, IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT, imageUxReviewProofEvidence, imageUxReviewCommandOutcome, buildImageUxCalloutExtractionReport, writeImageUxReviewRouteArtifacts } from '../image-ux-review.js';
8
8
  import { maybeFinalizeRoute } from '../proof/auto-finalize.js';
9
- import { generatedImageMetadata, generateGptImage2CalloutReview } from '../image-ux-review/imagegen-adapter.js';
9
+ import { buildCalloutPrompt, generatedImageMetadata, generateGptImage2CalloutReview } from '../image-ux-review/imagegen-adapter.js';
10
10
  import { extractRealCallouts } from '../image-ux-review/real-callout-extractor.js';
11
11
  import { addImageRelation, ingestImage } from '../wiki-image/image-voxel-ledger.js';
12
12
  import { sha256File, imageDimensions } from '../wiki-image/image-hash.js';
@@ -66,13 +66,17 @@ export async function imageUxReviewCommand(command, args = []) {
66
66
  async function runImageUxReview(root, command, args = []) {
67
67
  const missionRequested = readOption(args, '--mission', null);
68
68
  const missionId = missionRequested
69
- ? missionRequested === 'latest' ? await findLatestMission(root, { mode: 'image-ux-review', route: '$Image-UX-Review', gateFile: IMAGE_UX_REVIEW_GATE_ARTIFACT }) : missionRequested
69
+ ? missionRequested === 'latest' ? await findLatestMission(root, { mode: 'image-ux-review' }) : missionRequested
70
70
  : null;
71
71
  const imagePath = readOption(args, '--image', null) || readOption(args, '--screenshot', null);
72
72
  const generatedImage = readOption(args, '--generated-image', null);
73
- const shouldGenerateCallouts = flag(args, '--generate-callouts') || flag(args, '--fix');
74
- if (missionId)
73
+ const shouldGenerateCallouts = !generatedImage;
74
+ if (missionId) {
75
+ if (generatedImage) {
76
+ return continueExistingMissionWithGeneratedImage(root, command, missionId, generatedImage, args, 'sks.image-ux-review-run.v1');
77
+ }
75
78
  return rebuildExistingMission(root, command, [missionId, ...args], { fixRequested: flag(args, '--fix') });
79
+ }
76
80
  const sourcePreflight = await imageUxReviewSourcePreflight(args);
77
81
  const { fromChromeExtension, fromComputerUse, chromePreflight } = sourcePreflight;
78
82
  if (sourcePreflight.result) {
@@ -111,7 +115,7 @@ async function runImageUxReview(root, command, args = []) {
111
115
  process.exitCode = 1;
112
116
  if (flag(args, '--json'))
113
117
  return printJson(result);
114
- console.error('UX Review blocked: Codex App imagegen/gpt-image-2 is unavailable.');
118
+ console.error('UX Review blocked: no selected Codex imagegen/gpt-image-2 provider is ready.');
115
119
  for (const action of imagegenRequired.blocker?.next_actions || [])
116
120
  console.error(`- ${action}`);
117
121
  return result;
@@ -133,8 +137,16 @@ async function runImageUxReview(root, command, args = []) {
133
137
  }
134
138
  };
135
139
  await writeJsonAtomic(path.join(dir, 'decision-contract.json'), contract);
136
- if (generatedImage)
140
+ if (generatedImage) {
137
141
  await attachGeneratedReviewImage(root, dir, contract, generatedImage, { realGenerated: !flag(args, '--mock'), mock: flag(args, '--mock') });
142
+ if (!flag(args, '--mock')) {
143
+ await extractAndWriteGeneratedReview(root, dir, {
144
+ generatedImagePath: generatedImage,
145
+ sourceImagePath: sourceRel || imagePath,
146
+ sessionId: readOption(args, '--session', null) || readOption(args, '--session-id', null)
147
+ });
148
+ }
149
+ }
138
150
  if (!generatedImage && shouldGenerateCallouts) {
139
151
  const outputDir = path.join(dir, 'generated-callouts');
140
152
  const missionStartMs = Date.parse(mission.created_at || '') || undefined;
@@ -144,7 +156,9 @@ async function runImageUxReview(root, command, args = []) {
144
156
  source_screen_id: 'screen-1',
145
157
  source_image_path: path.resolve(root, sourceRel || imagePath),
146
158
  output_dir: outputDir,
147
- prompt: promptForRun(command, args),
159
+ prompt: buildCalloutPrompt('screen-1', {
160
+ target: contract.answers.TARGET_SURFACE
161
+ }),
148
162
  requested_fidelity: 'original',
149
163
  privacy: 'local-only'
150
164
  }, {
@@ -153,11 +167,11 @@ async function runImageUxReview(root, command, args = []) {
153
167
  generatedImageMaxAgeMs: maxAgeOverride ? Number(maxAgeOverride) * 60 * 1000 : 30 * 60 * 1000
154
168
  }
155
169
  });
170
+ if (result.request_artifact)
171
+ await fsp.copyFile(result.request_artifact, path.join(dir, IMAGE_UX_REVIEW_GPT_IMAGE_2_REQUEST_ARTIFACT));
172
+ if (result.response_artifact)
173
+ await fsp.copyFile(result.response_artifact, path.join(dir, IMAGE_UX_REVIEW_GPT_IMAGE_2_RESPONSE_ARTIFACT));
156
174
  if (result.generated_image_path) {
157
- if (result.request_artifact)
158
- await fsp.copyFile(result.request_artifact, path.join(dir, IMAGE_UX_REVIEW_GPT_IMAGE_2_REQUEST_ARTIFACT)).catch(() => { });
159
- if (result.response_artifact)
160
- await fsp.copyFile(result.response_artifact, path.join(dir, IMAGE_UX_REVIEW_GPT_IMAGE_2_RESPONSE_ARTIFACT)).catch(() => { });
161
175
  const response = await readImagegenResponse(dir);
162
176
  const evidenceClass = String(response?.evidence_class || '');
163
177
  const fakeGenerated = evidenceClass === 'mock_fixture' || result.provider === 'fake_imagegen_adapter';
@@ -171,25 +185,21 @@ async function runImageUxReview(root, command, args = []) {
171
185
  outputSha256: response?.output_sha256 || response?.output_image_sha256 || null,
172
186
  responseArtifact: result.response_artifact || null
173
187
  });
174
- const extraction = await extractRealCallouts({
175
- root,
188
+ await extractAndWriteGeneratedReview(root, dir, {
176
189
  generatedImagePath: result.generated_image_path,
177
- sourceScreenshot: { id: 'screen-1' },
190
+ sourceImagePath: sourceRel || imagePath,
178
191
  sessionId: readOption(args, '--session', null) || readOption(args, '--session-id', null)
179
192
  });
180
- await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT), extraction.issue_ledger);
181
- await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_CALLOUT_EXTRACTION_REPORT_ARTIFACT), await buildImageUxCalloutExtractionReport(root, extraction, {
182
- generatedImagePath: result.generated_image_path,
183
- sourceImagePath: sourceRel || imagePath,
184
- provider: extraction.provider
185
- }));
186
193
  }
187
194
  }
188
195
  const artifacts = await writeImageUxReviewRouteArtifacts(dir, contract, { root, wrongnessChecked: true, honestModeComplete: true });
189
196
  artifacts.gate = await enforceImageUxRuntimeGate(dir, artifacts.gate, { mock: flag(args, '--mock') });
190
197
  await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_GATE_ARTIFACT), artifacts.gate);
191
198
  const proof = await finalizeImageUx(root, id, command, artifacts, { mock: flag(args, '--mock'), cmd: `sks ${command} run` });
192
- const result = { schema: 'sks.image-ux-review-run.v1', ok: proof.ok && artifacts.gate?.passed === true, status: artifacts.gate?.status || (artifacts.gate?.passed ? 'passed' : 'blocked'), mission_id: id, artifacts, proof: proof.validation };
199
+ const outcome = imageUxReviewCommandOutcome(artifacts.gate, proof.ok, {
200
+ allowReviewOnlyCompletion: !flag(args, '--fix')
201
+ });
202
+ const result = { schema: 'sks.image-ux-review-run.v1', ...outcome, mission_id: id, artifacts, proof: proof.validation };
193
203
  if (!result.ok)
194
204
  process.exitCode = 1;
195
205
  if (flag(args, '--json'))
@@ -197,10 +207,67 @@ async function runImageUxReview(root, command, args = []) {
197
207
  console.log(`Image UX review: ${result.ok ? 'ok' : 'blocked'} ${id}`);
198
208
  return result;
199
209
  }
200
- export async function imageUxReviewSourcePreflight(args = []) {
210
+ async function extractAndWriteGeneratedReview(root, dir, input) {
211
+ const extraction = await extractRealCallouts({
212
+ root,
213
+ generatedImagePath: input.generatedImagePath,
214
+ sourceScreenshot: { id: 'screen-1' },
215
+ sessionId: input.sessionId || null
216
+ });
217
+ const generatedLedger = await readJson(path.join(dir, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT), null).catch(() => null);
218
+ const generatedImages = Array.isArray(generatedLedger?.generated_review_images) ? generatedLedger.generated_review_images : [];
219
+ const localImage = generatedImages.find((image) => image.sha256 && image.sha256 === extraction.generated_image_sha256)
220
+ || generatedImages[0]
221
+ || null;
222
+ const localImageId = localImage?.id || null;
223
+ const generatedSha = localImage?.sha256 || extraction.generated_image_sha256 || null;
224
+ const extractedIssues = Array.isArray(extraction.issue_ledger?.issues) ? extraction.issue_ledger.issues : [];
225
+ const boundIssues = extractedIssues.map((issue) => ({
226
+ ...issue,
227
+ generated_review_image_id: localImageId || issue.generated_review_image_id,
228
+ evidence_image_id: localImageId || issue.evidence_image_id,
229
+ generated_image_sha256: generatedSha || issue.generated_image_sha256
230
+ }));
231
+ const boundIssueLedger = {
232
+ ...extraction.issue_ledger,
233
+ generated_image_id: localImageId,
234
+ generated_image_sha256: generatedSha,
235
+ issues: boundIssues
236
+ };
237
+ const extractionSucceeded = extraction.ok === true
238
+ && boundIssueLedger.validation?.ok === true
239
+ && boundIssues.length > 0;
240
+ if (localImage && generatedLedger) {
241
+ await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT), {
242
+ ...generatedLedger,
243
+ status: extractionSucceeded ? 'generated_and_extracted' : 'generated_extraction_blocked',
244
+ generated_review_images: generatedImages.map((image) => image.id === localImage.id ? {
245
+ ...image,
246
+ generated_image_sha256: generatedSha,
247
+ extraction_provider: extraction.provider || null,
248
+ extraction_status: extraction.status || (extractionSucceeded ? 'extracted' : 'blocked'),
249
+ callout_extraction_status: extractionSucceeded ? 'succeeded' : 'failed',
250
+ callout_extraction_blocker: extractionSucceeded ? null : extraction.blocker || 'callout_extraction_failed',
251
+ callouts: boundIssues
252
+ } : image)
253
+ });
254
+ }
255
+ await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT), boundIssueLedger);
256
+ await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_CALLOUT_EXTRACTION_REPORT_ARTIFACT), await buildImageUxCalloutExtractionReport(root, extraction, {
257
+ generatedImagePath: input.generatedImagePath,
258
+ generatedImageId: localImageId,
259
+ sourceImagePath: input.sourceImagePath || null,
260
+ provider: extraction.provider
261
+ }));
262
+ return extraction;
263
+ }
264
+ export async function imageUxReviewSourcePreflight(args = [], opts = {}) {
201
265
  const fromChromeExtension = flag(args, '--from-chrome-extension') || Boolean(readOption(args, '--from-chrome-extension', null));
202
266
  const fromComputerUse = flag(args, '--from-computer-use') || Boolean(readOption(args, '--from-computer-use', null));
203
- const chromePreflight = fromChromeExtension ? await codexChromeExtensionStatus() : null;
267
+ const imagePath = readOption(args, '--image', null) || readOption(args, '--screenshot', null);
268
+ const chromePreflight = fromChromeExtension
269
+ ? await (opts.chromeStatus ? opts.chromeStatus() : codexChromeExtensionStatus())
270
+ : null;
204
271
  if (chromePreflight && !chromePreflight.ok) {
205
272
  return {
206
273
  fromChromeExtension,
@@ -231,6 +298,22 @@ export async function imageUxReviewSourcePreflight(args = []) {
231
298
  }
232
299
  };
233
300
  }
301
+ if ((fromChromeExtension || fromComputerUse) && !imagePath) {
302
+ return {
303
+ fromChromeExtension,
304
+ fromComputerUse,
305
+ chromePreflight,
306
+ result: {
307
+ schema: 'sks.image-ux-review-run.v1',
308
+ ok: false,
309
+ status: 'blocked',
310
+ blocker: 'captured_screenshot_path_required',
311
+ guidance: [
312
+ 'Capture the real page or native-app screen first, then pass its local artifact path with --image <captured-path>.'
313
+ ]
314
+ }
315
+ };
316
+ }
234
317
  return { fromChromeExtension, fromComputerUse, chromePreflight, result: null };
235
318
  }
236
319
  function printImageUxReviewSourcePreflightBlock(result) {
@@ -240,6 +323,10 @@ function printImageUxReviewSourcePreflightBlock(result) {
240
323
  console.error(result.chrome_extension.docs_url);
241
324
  return;
242
325
  }
326
+ if (result.blocker === 'captured_screenshot_path_required') {
327
+ console.error('UX Review blocked: capture completed without a bound screenshot artifact. Pass --image <captured-path>.');
328
+ return;
329
+ }
243
330
  console.error('UX Review blocked: web/browser UX review requires Codex Chrome Extension, not Computer Use. Use --from-chrome-extension after setup, or provide a screenshot with --image.');
244
331
  }
245
332
  async function calloutsImageUxReview(root, command, args = []) {
@@ -255,8 +342,12 @@ async function calloutsImageUxReview(root, command, args = []) {
255
342
  return runImageUxReview(root, command, ['--image', imagePath, '--generate-callouts', '--json', ...(flag(args, '--mock') ? ['--mock'] : [])]);
256
343
  }
257
344
  async function extractIssuesImageUxReview(root, command, args = []) {
345
+ const missionRequested = readOption(args, '--mission', null);
346
+ const missionId = missionRequested
347
+ ? missionRequested === 'latest' ? await findLatestMission(root, { mode: 'image-ux-review' }) : missionRequested
348
+ : null;
258
349
  const generatedImage = readOption(args, '--generated-image', null);
259
- const sourceImage = readOption(args, '--image', null) || readOption(args, '--screenshot', null) || generatedImage;
350
+ const sourceImage = readOption(args, '--image', null) || readOption(args, '--screenshot', null);
260
351
  if (!generatedImage) {
261
352
  const result = { schema: 'sks.image-ux-review-extract-issues.v1', ok: false, status: 'blocked', blocker: 'generated_image_required' };
262
353
  process.exitCode = 1;
@@ -265,6 +356,9 @@ async function extractIssuesImageUxReview(root, command, args = []) {
265
356
  console.error('Usage: sks ux-review extract-issues --generated-image <path> --json');
266
357
  return result;
267
358
  }
359
+ if (missionId) {
360
+ return continueExistingMissionWithGeneratedImage(root, command, missionId, generatedImage, args, 'sks.image-ux-review-extract-issues.v1');
361
+ }
268
362
  const { id, dir, mission } = await createMission(root, { mode: 'image-ux-review', prompt: `Extract UX issues from ${generatedImage}` });
269
363
  const sourceRel = sourceImage ? await stageSourceImage(root, dir, sourceImage) : null;
270
364
  const contract = {
@@ -275,18 +369,11 @@ async function extractIssuesImageUxReview(root, command, args = []) {
275
369
  await writeJsonAtomic(path.join(dir, 'decision-contract.json'), contract);
276
370
  await attachGeneratedReviewImage(root, dir, contract, generatedImage, { realGenerated: !flag(args, '--mock'), mock: flag(args, '--mock') });
277
371
  if (!flag(args, '--mock')) {
278
- const extraction = await extractRealCallouts({
279
- root,
372
+ await extractAndWriteGeneratedReview(root, dir, {
280
373
  generatedImagePath: generatedImage,
281
- sourceScreenshot: { id: 'screen-1' },
374
+ sourceImagePath: sourceRel || sourceImage,
282
375
  sessionId: readOption(args, '--session', null) || readOption(args, '--session-id', null)
283
376
  });
284
- await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT), extraction.issue_ledger);
285
- await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_CALLOUT_EXTRACTION_REPORT_ARTIFACT), await buildImageUxCalloutExtractionReport(root, extraction, {
286
- generatedImagePath: generatedImage,
287
- sourceImagePath: sourceRel || sourceImage,
288
- provider: extraction.provider
289
- }));
290
377
  }
291
378
  const artifacts = await writeImageUxReviewRouteArtifacts(dir, contract, { root, wrongnessChecked: true, honestModeComplete: true });
292
379
  const proof = await finalizeImageUx(root, id, command, artifacts, { mock: flag(args, '--mock'), cmd: `sks ${command} extract-issues` });
@@ -298,9 +385,56 @@ async function extractIssuesImageUxReview(root, command, args = []) {
298
385
  console.log(`Image UX issue extraction: ${result.ok ? 'ok' : 'blocked'} ${id}`);
299
386
  return result;
300
387
  }
388
+ async function continueExistingMissionWithGeneratedImage(root, command, missionId, generatedImage, args, schema) {
389
+ const { dir, mission } = await loadMission(root, missionId);
390
+ const contract = await readJson(path.join(dir, 'decision-contract.json'), { prompt: mission.prompt, answers: {}, sealed_hash: null });
391
+ const mock = flag(args, '--mock');
392
+ const ledger = await attachGeneratedReviewImage(root, dir, contract, generatedImage, {
393
+ realGenerated: !mock,
394
+ mock
395
+ });
396
+ if (!mock) {
397
+ await extractAndWriteGeneratedReview(root, dir, {
398
+ generatedImagePath: generatedImage,
399
+ sourceImagePath: sourceImageFromContract(contract),
400
+ sessionId: readOption(args, '--session', null) || readOption(args, '--session-id', null)
401
+ });
402
+ }
403
+ const artifacts = await writeImageUxReviewRouteArtifacts(dir, contract, {
404
+ root,
405
+ wrongnessChecked: true,
406
+ honestModeComplete: true
407
+ });
408
+ artifacts.gate = await enforceImageUxRuntimeGate(dir, artifacts.gate, { mock });
409
+ await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_GATE_ARTIFACT), artifacts.gate);
410
+ const proof = await finalizeImageUx(root, missionId, command, artifacts, {
411
+ mock,
412
+ cmd: `sks ${command} ${schema.endsWith('extract-issues.v1') ? 'extract-issues' : 'attach-generated'}`
413
+ });
414
+ const outcome = imageUxReviewCommandOutcome(artifacts.gate, proof.ok, {
415
+ allowReviewOnlyCompletion: !flag(args, '--fix')
416
+ });
417
+ const result = {
418
+ schema,
419
+ ...outcome,
420
+ mission_id: missionId,
421
+ generated_review_ledger: ledger,
422
+ issue_ledger: artifacts.issue_ledger,
423
+ gate: artifacts.gate,
424
+ proof: proof.validation
425
+ };
426
+ if (!result.ok)
427
+ process.exitCode = 1;
428
+ if (flag(args, '--json'))
429
+ return printJson(result);
430
+ console.log(`Image UX review continued: ${result.ok ? 'ok' : 'blocked'} ${missionId}`);
431
+ return result;
432
+ }
301
433
  async function attachGeneratedImageCommand(root, command, args = []) {
302
- const missionArg = args.find((arg) => !String(arg).startsWith('--')) || 'latest';
303
- const missionId = missionArg === 'latest' ? await findLatestMission(root, { mode: 'image-ux-review', route: '$Image-UX-Review', gateFile: IMAGE_UX_REVIEW_GATE_ARTIFACT }) : missionArg;
434
+ const missionArg = readOption(args, '--mission', null)
435
+ || args.find((arg, index) => !String(arg).startsWith('--') && !String(args[index - 1] || '').startsWith('--'))
436
+ || 'latest';
437
+ const missionId = missionArg === 'latest' ? await findLatestMission(root, { mode: 'image-ux-review' }) : missionArg;
304
438
  const imagePath = readOption(args, '--image', null) || readOption(args, '--generated-image', null);
305
439
  if (!missionId || !imagePath) {
306
440
  const result = { schema: 'sks.image-ux-review-attach-generated.v1', ok: false, status: 'blocked', blocker: !missionId ? 'mission_required' : 'generated_image_required' };
@@ -309,15 +443,7 @@ async function attachGeneratedImageCommand(root, command, args = []) {
309
443
  return printJson(result);
310
444
  return result;
311
445
  }
312
- const { dir, mission } = await loadMission(root, missionId);
313
- const contract = await readJson(path.join(dir, 'decision-contract.json'), { prompt: mission.prompt, answers: {}, sealed_hash: null });
314
- const ledger = await attachGeneratedReviewImage(root, dir, contract, imagePath, { realGenerated: !flag(args, '--mock'), mock: flag(args, '--mock') });
315
- const artifacts = await writeImageUxReviewRouteArtifacts(dir, contract, { root, wrongnessChecked: true, honestModeComplete: true });
316
- const result = { schema: 'sks.image-ux-review-attach-generated.v1', ok: artifacts.gate.generated_image_ingested === true, mission_id: missionId, generated_review_ledger: ledger, gate: artifacts.gate };
317
- if (flag(args, '--json'))
318
- return printJson(result);
319
- console.log(`Generated image attached: ${missionId}`);
320
- return result;
446
+ return continueExistingMissionWithGeneratedImage(root, command, missionId, imagePath, args, 'sks.image-ux-review-attach-generated.v1');
321
447
  }
322
448
  async function attachAfterImageCommand(root, command, args = []) {
323
449
  const missionArg = args.find((arg) => !String(arg).startsWith('--')) || 'latest';
@@ -365,7 +491,10 @@ async function rebuildExistingMission(root, command, args = [], opts = {}) {
365
491
  artifacts.gate = await enforceImageUxRuntimeGate(dir, artifacts.gate, { mock: flag(args, '--mock') });
366
492
  await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_GATE_ARTIFACT), artifacts.gate);
367
493
  const proof = await finalizeImageUx(root, missionId, command, artifacts, { mock: flag(args, '--mock'), cmd: `sks ${command} ${opts.fixRequested ? 'fix' : opts.recaptureRequested ? 'recapture' : 'build'}` });
368
- const result = { schema: 'sks.image-ux-review-build.v2', ok: proof.ok && artifacts.gate?.passed === true, status: artifacts.gate?.status || (artifacts.gate?.passed ? 'passed' : 'blocked'), mission_id: missionId, artifacts, proof: proof.validation };
494
+ const outcome = imageUxReviewCommandOutcome(artifacts.gate, proof.ok, {
495
+ allowReviewOnlyCompletion: false
496
+ });
497
+ const result = { schema: 'sks.image-ux-review-build.v2', ...outcome, mission_id: missionId, artifacts, proof: proof.validation };
369
498
  if (!result.ok)
370
499
  process.exitCode = 1;
371
500
  if (flag(args, '--json'))
@@ -510,10 +639,19 @@ async function attachGeneratedReviewImage(root, dir, contract, imagePath, opts =
510
639
  evidence_class: evidenceClass,
511
640
  output_source: outputSource,
512
641
  output_sha256: outputSha256 || undefined,
642
+ output_id: response?.output_id || null,
513
643
  real_generated: opts.realGenerated === true && isFullImagegenEvidenceClass(evidenceClass),
514
644
  mock: opts.mock === true
515
645
  });
516
- if (!response && opts.realGenerated === true) {
646
+ if (response) {
647
+ await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_GPT_IMAGE_2_RESPONSE_ARTIFACT), {
648
+ ...response,
649
+ generated_review_image_id: metadata.id,
650
+ output_sha256: response.output_sha256 || response.output_image_sha256 || metadata.sha256,
651
+ output_image_sha256: response.output_image_sha256 || response.output_sha256 || metadata.sha256
652
+ });
653
+ }
654
+ else if (opts.realGenerated === true) {
517
655
  await writeJsonAtomic(path.join(dir, IMAGE_UX_REVIEW_GPT_IMAGE_2_RESPONSE_ARTIFACT), {
518
656
  schema: 'sks.image-ux-gpt-image-2-response.v1',
519
657
  created_at: nowIso(),
@@ -526,6 +664,7 @@ async function attachGeneratedReviewImage(root, dir, contract, imagePath, opts =
526
664
  output_image_sha256: metadata.sha256,
527
665
  output_sha256: metadata.sha256,
528
666
  output_id: metadata.output_id || null,
667
+ generated_review_image_id: metadata.id,
529
668
  output_source: 'manual_attach',
530
669
  dimensions: { width: metadata.width, height: metadata.height, format: metadata.format },
531
670
  local_only: true
@@ -582,7 +721,9 @@ async function readImagegenResponse(dir) {
582
721
  }
583
722
  async function enforceImageUxRuntimeGate(dir, gate = {}, opts = {}) {
584
723
  const inventory = await readJson(path.join(dir, IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT), null);
724
+ const generatedLedger = await readJson(path.join(dir, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT), null);
585
725
  const issueLedger = await readJson(path.join(dir, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT), null);
726
+ const extractionReport = await readJson(path.join(dir, IMAGE_UX_REVIEW_CALLOUT_EXTRACTION_REPORT_ARTIFACT), null);
586
727
  const response = await readJson(path.join(dir, IMAGE_UX_REVIEW_GPT_IMAGE_2_RESPONSE_ARTIFACT), null);
587
728
  const blockers = new Set(Array.isArray(gate?.blockers) ? gate.blockers.map(String) : []);
588
729
  const sourceScreens = Array.isArray(inventory?.source_screens) ? inventory.source_screens : [];
@@ -606,19 +747,34 @@ async function enforceImageUxRuntimeGate(dir, gate = {}, opts = {}) {
606
747
  const responseEvidence = await validateImagegenResponseEvidence(response, dir);
607
748
  for (const blocker of responseEvidence.blockers)
608
749
  blockers.add(blocker);
750
+ const bindingBlockers = validateGeneratedImageEvidenceBinding(response, generatedLedger, extractionReport, issueLedger);
751
+ for (const blocker of bindingBlockers)
752
+ blockers.add(blocker);
609
753
  const nextBlockers = [...blockers];
610
754
  const codexGenerated = responseEvidence.ok === true && gate?.gpt_image_2_callout_generated === true;
755
+ const sourceScreenshotMinResolutionPassed = sourceScreens.length > 0
756
+ && sourceScreens.every((screen) => Number(screen?.width || 0) >= 64 && Number(screen?.height || 0) >= 64);
757
+ const issueLedgerRealExtraction = issues.length > 0
758
+ && issues.every((issue) => issue?.extraction_provider !== 'mock_fixture' && issue?.source !== 'mock_fixture');
759
+ const reviewReportCompleted = gate?.review_report_completed === true
760
+ && codexGenerated
761
+ && bindingBlockers.length === 0
762
+ && sourceScreenshotMinResolutionPassed
763
+ && issueLedgerRealExtraction;
611
764
  const passed = gate?.passed === true && codexGenerated && nextBlockers.length === 0;
612
765
  return {
613
766
  ...gate,
614
767
  passed,
615
768
  ok: passed,
616
- status: passed ? 'passed' : 'blocked',
769
+ status: passed ? 'passed' : reviewReportCompleted ? 'review_completed' : 'blocked',
770
+ verified_level: passed ? gate?.verified_level || 'verified' : reviewReportCompleted ? 'verified_partial' : 'blocked',
771
+ review_report_completed: reviewReportCompleted,
772
+ completion_scope: reviewReportCompleted ? 'capture_imagegen_ocr_and_ux_report' : gate?.completion_scope || null,
617
773
  gpt_image_2_callout_generated: codexGenerated,
618
774
  generated_image_evidence: responseEvidence.ok === true,
619
775
  imagegen_response_evidence: responseEvidence,
620
- source_screenshot_min_resolution_passed: sourceScreens.length > 0 && sourceScreens.every((screen) => Number(screen?.width || 0) >= 64 && Number(screen?.height || 0) >= 64),
621
- issue_ledger_real_extraction: issues.length > 0 && issues.every((issue) => issue?.extraction_provider !== 'mock_fixture' && issue?.source !== 'mock_fixture'),
776
+ source_screenshot_min_resolution_passed: sourceScreenshotMinResolutionPassed,
777
+ issue_ledger_real_extraction: issueLedgerRealExtraction,
622
778
  blockers: nextBlockers
623
779
  };
624
780
  }
@@ -631,6 +787,9 @@ async function validateImagegenResponseEvidence(response = null, dir = process.c
631
787
  blockers.push('imagegen_response_schema_invalid');
632
788
  if (response.ok !== true || response.status !== 'generated')
633
789
  blockers.push(response.blocker || 'imagegen_response_not_generated');
790
+ if (response.image_output_partial_frame === true || response.payload_summary?.image_output_partial_frame === true) {
791
+ blockers.push('imagegen_response_partial_image_output');
792
+ }
634
793
  const evidenceClass = String(response.evidence_class || '');
635
794
  blockers.push(...imagegenEvidenceClassBlockers('imagegen_response', evidenceClass));
636
795
  const source = String(response.output_source || '');
@@ -638,6 +797,9 @@ async function validateImagegenResponseEvidence(response = null, dir = process.c
638
797
  blockers.push('imagegen_response_output_source_invalid');
639
798
  const outputPath = String(response.output_image_path || '');
640
799
  const expectedSha = String(response.output_sha256 || response.output_image_sha256 || '');
800
+ const generatedReviewImageId = String(response.generated_review_image_id || '');
801
+ if (!generatedReviewImageId)
802
+ blockers.push('imagegen_response_generated_review_image_id_missing');
641
803
  if (!outputPath)
642
804
  blockers.push('imagegen_response_output_path_missing');
643
805
  if (!expectedSha)
@@ -665,9 +827,36 @@ async function validateImagegenResponseEvidence(response = null, dir = process.c
665
827
  output_source: source || null,
666
828
  output_image_path: outputPath || null,
667
829
  output_sha256: expectedSha || null,
830
+ generated_review_image_id: generatedReviewImageId || null,
668
831
  blockers: [...new Set(blockers)]
669
832
  };
670
833
  }
834
+ function validateGeneratedImageEvidenceBinding(response, generatedLedger, extractionReport, issueLedger) {
835
+ if (!response || response.ok !== true || response.status !== 'generated')
836
+ return [];
837
+ const blockers = [];
838
+ const generatedId = String(response.generated_review_image_id || '');
839
+ const responseSha = String(response.output_sha256 || response.output_image_sha256 || '');
840
+ const images = Array.isArray(generatedLedger?.generated_review_images) ? generatedLedger.generated_review_images : [];
841
+ const image = images.find((row) => String(row?.id || '') === generatedId);
842
+ if (generatedId && !image)
843
+ blockers.push('imagegen_response_generated_review_image_id_not_in_ledger');
844
+ if (image && responseSha && String(image.sha256 || '') !== responseSha)
845
+ blockers.push('imagegen_response_generated_review_image_sha256_mismatch');
846
+ if (extractionReport) {
847
+ if (String(extractionReport.generated_image_id || '') !== generatedId)
848
+ blockers.push('callout_extraction_generated_image_id_mismatch');
849
+ if (responseSha && String(extractionReport.generated_image_sha256 || '') !== responseSha)
850
+ blockers.push('callout_extraction_generated_image_sha256_mismatch');
851
+ }
852
+ for (const issue of Array.isArray(issueLedger?.issues) ? issueLedger.issues : []) {
853
+ if (String(issue.generated_review_image_id || '') !== generatedId)
854
+ blockers.push(`issue_generated_image_id_mismatch:${issue.id || 'unknown'}`);
855
+ if (responseSha && String(issue.generated_image_sha256 || '') !== responseSha)
856
+ blockers.push(`issue_generated_image_sha256_mismatch:${issue.id || 'unknown'}`);
857
+ }
858
+ return [...new Set(blockers)];
859
+ }
671
860
  function rootFromMissionDir(dir) {
672
861
  const marker = `${path.sep}.sneakoscope${path.sep}missions${path.sep}`;
673
862
  const idx = String(dir).indexOf(marker);
@@ -738,6 +927,10 @@ function promptForRun(command, args) {
738
927
  const source = readOption(args, '--image', null) || readOption(args, '--screenshot', null) || readOption(args, '--mission', null) || 'latest Codex Chrome Extension or native Computer Use screenshot';
739
928
  return `$${routeForCommand(command).replace(/^\$/, '')} ${source} with gpt-image-2 callouts${flag(args, '--fix') ? ', then fix the issues' : ''}`;
740
929
  }
930
+ function sourceImageFromContract(contract) {
931
+ const sources = contract?.answers?.IMAGE_UX_REVIEW_SOURCE_IMAGES;
932
+ return Array.isArray(sources) && sources.length ? String(sources[0]) : null;
933
+ }
741
934
  function missingMission(args) {
742
935
  const result = { schema: 'sks.image-ux-review-status.v2', ok: false, status: 'missing_mission' };
743
936
  if (flag(args, '--json'))