sneakoscope 3.1.8 → 3.1.9

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.
@@ -1,12 +1,16 @@
1
1
  import path from 'node:path';
2
2
  import { exists, nowIso, readJson, readText, writeJsonAtomic, writeTextAtomic, PACKAGE_VERSION } from './fsx.js';
3
- import { CODEX_WEB_VERIFICATION_EVIDENCE_SOURCE, CODEX_WEB_VERIFICATION_POLICY, evidenceMentionsForbiddenBrowserAutomation, evidenceMentionsForbiddenWebComputerUseEvidence } from './routes.js';
3
+ import { CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_IMAGEGEN_REQUIRED_POLICY, CODEX_WEB_VERIFICATION_EVIDENCE_SOURCE, CODEX_WEB_VERIFICATION_POLICY, evidenceMentionsForbiddenBrowserAutomation, evidenceMentionsForbiddenWebComputerUseEvidence } from './routes.js';
4
4
  import { appendAgentLedgerEvent, initializeAgentCentralLedger } from './agents/agent-central-ledger.js';
5
5
  import { resolveCodexAppExecutionProfile } from './codex-app/codex-app-execution-profile.js';
6
6
  import { resolveCodexNativeInvocationPlan } from './codex-native/codex-native-invocation-router.js';
7
+ import { imageDimensions, sha256File } from './wiki-image/image-hash.js';
7
8
  export const QA_LOOP_ROUTE = 'QALoop';
9
+ export const QA_LOOP_VISUAL_EVIDENCE_ARTIFACT = 'qa-loop/visual-evidence.json';
8
10
  const QA_REPORT_SUFFIX = 'qa-report.md';
9
11
  const UI_CHROME_EXTENSION_FIRST_ACK = 'use_codex_chrome_extension_first_no_computer_use_for_web_ui_or_mark_unverified';
12
+ const GPT_IMAGE_2_ANNOTATED_REVIEW_REQUIRED_ACK = 'yes_gpt_image_2_annotated_review';
13
+ const IMAGE_FILE_RE = /\.(png|jpe?g|webp|gif)$/i;
10
14
  export const QA_NATIVE_AGENT_PERSONAS = Object.freeze([
11
15
  {
12
16
  id: 'qa_verifier_ui',
@@ -108,6 +112,9 @@ function promptText(prompt = '') {
108
112
  function lowerPrompt(prompt = '') {
109
113
  return promptText(prompt).toLowerCase();
110
114
  }
115
+ function qaPromptWantsGptImage2AnnotatedReview(prompt = '') {
116
+ return /(gpt-image-2|gpt\s*image\s*2|imagegen|\$imagegen|annotated\s+review|annotated\s+image|callout|generated\s+review\s+image|이미지\s*리뷰|생성\s*이미지|주석\s*이미지|콜아웃)/i.test(promptText(prompt));
117
+ }
111
118
  function firstUrl(prompt = '') {
112
119
  return promptText(prompt).match(/https?:\/\/[^\s)\]}>,]+/i)?.[0] || '';
113
120
  }
@@ -152,6 +159,16 @@ export function inferQaLoopAnswers(prompt = '') {
152
159
  const local = environment === 'local_dev_server';
153
160
  const login = loginPolicyFromPrompt(text);
154
161
  const scope = qaScopeFromPrompt(text);
162
+ const wantsGptImage2Review = isUiScope(scope) && qaPromptWantsGptImage2AnnotatedReview(text);
163
+ const acceptance = [
164
+ '앱 첫 화면 또는 지정된 대상이 정상 로드된다.',
165
+ '주요 내비게이션과 핵심 화면 진입에서 콘솔/화면상 치명 오류가 없다.',
166
+ '검증하지 못한 UI/API 범위는 통과로 주장하지 않고 QA 리포트에 남긴다.'
167
+ ];
168
+ if (isUiScope(scope))
169
+ acceptance.push('UI E2E 통과 증거는 실제 Codex Chrome Extension screenshot artifact path와 sha256을 기록해야 한다.');
170
+ if (wantsGptImage2Review)
171
+ acceptance.push('gpt-image-2 annotated review image가 필요한 경우 실제 Codex App $imagegen/gpt-image-2 출력 파일 path, sha256, model, provider를 기록해야 한다.');
155
172
  return {
156
173
  GOAL_PRECISE: text ? `현재 요청 범위에서 QA-LOOP를 안전하게 실행한다: ${text}` : '현재 로컬 개발 환경에서 핵심 사용자 흐름을 안전하게 QA한다.',
157
174
  QA_SCOPE: scope,
@@ -165,13 +182,10 @@ export function inferQaLoopAnswers(prompt = '') {
165
182
  ...login,
166
183
  CREDENTIAL_STORAGE_ACK: 'never_store_credentials_in_artifacts_or_wiki',
167
184
  UI_CHROME_EXTENSION_ACK: UI_CHROME_EXTENSION_FIRST_ACK,
185
+ QA_VISUAL_REVIEW_IMAGEGEN_REQUIRED: wantsGptImage2Review ? GPT_IMAGE_2_ANNOTATED_REVIEW_REQUIRED_ACK : 'not_required',
168
186
  TEAM_MODE_ALLOWED: 'no_parent_only',
169
187
  MAX_QA_CYCLES: '1',
170
- ACCEPTANCE_CRITERIA: [
171
- '앱 첫 화면 또는 지정된 대상이 정상 로드된다.',
172
- '주요 내비게이션과 핵심 화면 진입에서 콘솔/화면상 치명 오류가 없다.',
173
- '검증하지 못한 UI/API 범위는 통과로 주장하지 않고 QA 리포트에 남긴다.'
174
- ],
188
+ ACCEPTANCE_CRITERIA: acceptance,
175
189
  NON_GOALS: [
176
190
  '결제, 실제 이메일/SMS 발송, 관리자 권한 변경, 데이터 삭제, 프로덕션 데이터 변경은 테스트하지 않는다.'
177
191
  ],
@@ -290,10 +304,22 @@ export function qaUiRequired(a = {}) {
290
304
  export function qaApiRequired(a = {}) {
291
305
  return a.QA_SCOPE === 'all_available' ? hasApiTarget(a) : isApiScope(a.QA_SCOPE);
292
306
  }
307
+ export function qaGptImage2AnnotatedReviewRequired(contractOrAnswers = {}, prompt = '') {
308
+ const answers = contractOrAnswers?.answers || contractOrAnswers || {};
309
+ if (!qaUiRequired(answers))
310
+ return false;
311
+ const explicit = String(answers.QA_VISUAL_REVIEW_IMAGEGEN_REQUIRED || answers.GPT_IMAGE_2_ANNOTATED_REVIEW_REQUIRED || '').trim();
312
+ if (/^(yes|true|required|yes_gpt_image_2_annotated_review)$/i.test(explicit))
313
+ return true;
314
+ if (/^(no|false|not_required|none)$/i.test(explicit))
315
+ return false;
316
+ return qaPromptWantsGptImage2AnnotatedReview(`${prompt || ''}\n${answers.GOAL_PRECISE || ''}\n${JSON.stringify(answers.ACCEPTANCE_CRITERIA || [])}`);
317
+ }
293
318
  export function defaultQaGate(contract = {}, opts = {}) {
294
319
  const a = contract.answers || {};
295
320
  const uiRequired = qaUiRequired(a);
296
321
  const apiRequired = qaApiRequired(a);
322
+ const gptImage2ReviewRequired = qaGptImage2AnnotatedReviewRequired(contract, contract.prompt);
297
323
  const reportFile = opts.reportFile || qaReportFilename();
298
324
  const corrective = a.QA_CORRECTIVE_POLICY !== 'report_only_no_code_changes';
299
325
  return {
@@ -311,6 +337,17 @@ export function defaultQaGate(contract = {}, opts = {}) {
311
337
  ui_chrome_extension_evidence: !uiRequired,
312
338
  ui_computer_use_evidence: false,
313
339
  ui_evidence_source: uiRequired ? null : 'not_required',
340
+ ui_chrome_extension_screenshot_required: uiRequired,
341
+ ui_chrome_extension_screenshot_captured: !uiRequired,
342
+ ui_chrome_extension_screenshot_artifact: null,
343
+ ui_chrome_extension_screenshot_sha256: null,
344
+ gpt_image_2_annotated_review_required: gptImage2ReviewRequired,
345
+ gpt_image_2_annotated_review_generated: !gptImage2ReviewRequired,
346
+ gpt_image_2_annotated_review_artifact: null,
347
+ gpt_image_2_annotated_review_sha256: null,
348
+ gpt_image_2_annotated_review_model: gptImage2ReviewRequired ? null : 'not_required',
349
+ gpt_image_2_annotated_review_provider: gptImage2ReviewRequired ? null : 'not_required',
350
+ qa_visual_evidence_artifact: QA_LOOP_VISUAL_EVIDENCE_ARTIFACT,
314
351
  desktop_app_handoff_required: false,
315
352
  desktop_app_handoff_status: 'not_requested',
316
353
  desktop_app_handoff_artifact: null,
@@ -360,13 +397,48 @@ export async function writeQaLoopArtifacts(dir, mission, contract) {
360
397
  codex_app_execution_profile: executionProfile ? compactExecutionProfile(executionProfile) : null,
361
398
  codex_native_invocation: codexNativeInvocation,
362
399
  target: { scope: a.QA_SCOPE, environment: a.TARGET_ENVIRONMENT, base_url: a.TARGET_BASE_URL, api_base_url: a.API_BASE_URL },
363
- safety: { mutation_policy: a.QA_MUTATION_POLICY, deployed_destructive_tests_allowed: 'never', credentials: 'temp_only_never_saved', ui_evidence: 'codex_chrome_extension_first_required_for_web_ui_e2e' },
400
+ safety: { mutation_policy: a.QA_MUTATION_POLICY, deployed_destructive_tests_allowed: 'never', credentials: 'temp_only_never_saved', ui_evidence: 'codex_chrome_extension_first_required_for_web_ui_e2e', visual_review: 'gpt_image_2_annotated_review_required_when_contract_requests_it' },
364
401
  checklist
365
402
  });
403
+ await writeJsonAtomic(path.join(dir, QA_LOOP_VISUAL_EVIDENCE_ARTIFACT), buildQaLoopVisualEvidenceArtifact(mission, contract));
366
404
  await writeJsonAtomic(path.join(dir, 'qa-gate.json'), defaultQaGate(contract, { reportFile, executionProfile, codexNativeInvocation }));
367
405
  await writeTextAtomic(path.join(dir, reportFile), qaReportTemplate(mission, contract, checklist));
368
406
  return { checklist_count: checklist.length, report_file: reportFile };
369
407
  }
408
+ export async function ensureQaLoopVisualEvidenceContract(dir, mission = {}, contract = {}) {
409
+ const visualPath = path.join(dir, QA_LOOP_VISUAL_EVIDENCE_ARTIFACT);
410
+ if (!(await exists(visualPath))) {
411
+ await writeJsonAtomic(visualPath, buildQaLoopVisualEvidenceArtifact(mission, contract));
412
+ }
413
+ const gatePath = path.join(dir, 'qa-gate.json');
414
+ const gate = await readJson(gatePath, null);
415
+ if (!gate)
416
+ return;
417
+ const defaults = defaultQaGate(contract, { reportFile: qaReportFileFromGate(gate) || qaReportFilename() });
418
+ const keys = [
419
+ 'ui_chrome_extension_screenshot_required',
420
+ 'ui_chrome_extension_screenshot_captured',
421
+ 'ui_chrome_extension_screenshot_artifact',
422
+ 'ui_chrome_extension_screenshot_sha256',
423
+ 'gpt_image_2_annotated_review_required',
424
+ 'gpt_image_2_annotated_review_generated',
425
+ 'gpt_image_2_annotated_review_artifact',
426
+ 'gpt_image_2_annotated_review_sha256',
427
+ 'gpt_image_2_annotated_review_model',
428
+ 'gpt_image_2_annotated_review_provider',
429
+ 'qa_visual_evidence_artifact'
430
+ ];
431
+ const next = { ...gate };
432
+ let changed = false;
433
+ for (const key of keys) {
434
+ if (next[key] === undefined) {
435
+ next[key] = defaults[key];
436
+ changed = true;
437
+ }
438
+ }
439
+ if (changed)
440
+ await writeJsonAtomic(gatePath, next);
441
+ }
370
442
  export async function evaluateQaGate(dir) {
371
443
  const gate = await readJson(path.join(dir, 'qa-gate.json'), {});
372
444
  const reportFile = qaReportFileFromGate(gate);
@@ -400,6 +472,10 @@ export async function evaluateQaGate(dir) {
400
472
  reasons.push('forbidden_browser_automation_evidence');
401
473
  if (evidenceMentionsForbiddenWebComputerUseEvidence({ evidence: gate.evidence, ui_evidence_source: gate.ui_evidence_source }))
402
474
  reasons.push('computer_use_web_evidence_forbidden');
475
+ reasons.push(...await missingQaLoopVisualEvidence(dir, gate));
476
+ }
477
+ else if (gate.gpt_image_2_annotated_review_required === true) {
478
+ reasons.push(...await missingQaLoopVisualEvidence(dir, gate));
403
479
  }
404
480
  if (gate.desktop_app_handoff_required === true) {
405
481
  if (!['pending', 'launched_pending_confirmation', 'completed'].includes(String(gate.desktop_app_handoff_status || '')))
@@ -424,8 +500,9 @@ export async function evaluateQaGate(dir) {
424
500
  reasons.push('qa_report_missing');
425
501
  if (!(await exists(path.join(dir, 'qa-ledger.json'))))
426
502
  reasons.push('qa_ledger_missing');
427
- const passed = gate.passed === true && reasons.length === 0;
428
- const result = { checked_at: nowIso(), passed, reasons, gate };
503
+ const uniqueReasons = [...new Set(reasons)];
504
+ const passed = gate.passed === true && uniqueReasons.length === 0;
505
+ const result = { checked_at: nowIso(), passed, reasons: uniqueReasons, gate };
429
506
  await writeJsonAtomic(path.join(dir, 'qa-gate.evaluated.json'), result);
430
507
  return result;
431
508
  }
@@ -514,12 +591,19 @@ ARTIFACTS: update qa-ledger.json, ${report}, qa-gate.json, and qa-loop/cycle-${c
514
591
  CONTRACT:
515
592
  ${JSON.stringify(contract, null, 2)}
516
593
  ${imageContractText}${appHandoffText}${executionProfileText}
594
+ VISUAL EVIDENCE CONTRACT:
595
+ - For web UI QA, do not set chrome_extension_preflight_passed/ui_chrome_extension_evidence to true unless the Codex Chrome Extension path is ready and ${QA_LOOP_VISUAL_EVIDENCE_ARTIFACT} records a real saved Chrome Extension screenshot artifact with path, sha256, and dimensions.
596
+ - If decision-contract.json answers set QA_VISUAL_REVIEW_IMAGEGEN_REQUIRED=${GPT_IMAGE_2_ANNOTATED_REVIEW_REQUIRED_ACK}, use Codex App $imagegen/gpt-image-2 (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) to produce a real generated annotated review image from the Chrome Extension screenshot. Record its path, sha256, model=gpt-image-2, provider=Codex App $imagegen, and source_screenshot_artifact in ${QA_LOOP_VISUAL_EVIDENCE_ARTIFACT} and qa-gate.json.
597
+ - Do not substitute prose-only critique, Playwright/Selenium/Puppeteer/Browser Use screenshots, Computer Use browser screenshots, placeholder images, fake fixtures, or direct API fallback as full web UI visual evidence.
517
598
  Previous tail:
518
599
  ${String(previous || '').slice(-2500)}
519
600
  `;
520
601
  }
521
602
  export async function qaStatus(dir) {
522
- const gate = await readJson(path.join(dir, 'qa-gate.evaluated.json'), await readJson(path.join(dir, 'qa-gate.json'), null));
603
+ const mission = await readJson(path.join(dir, 'mission.json'), {});
604
+ const contract = await readJson(path.join(dir, 'decision-contract.json'), { prompt: mission.prompt, answers: {}, sealed_hash: null });
605
+ await ensureQaLoopVisualEvidenceContract(dir, mission, contract).catch(() => undefined);
606
+ const gate = await evaluateQaGate(dir).catch(async () => await readJson(path.join(dir, 'qa-gate.evaluated.json'), await readJson(path.join(dir, 'qa-gate.json'), null)));
523
607
  const ledger = await readJson(path.join(dir, 'qa-ledger.json'), null);
524
608
  const appHandoff = await readJson(path.join(dir, 'qa-loop', 'app-handoff.json'), null);
525
609
  const appConfirmation = await readJson(path.join(dir, 'qa-loop', 'app-handoff-confirmation.json'), null);
@@ -545,6 +629,138 @@ function qaChecklist(a) {
545
629
  cases.push(['report.evidence', 'Record pass/fail/blocked/skipped with evidence.'], ['report.corrective_loop', 'Record fixes, rechecks, unresolved findings, deferred blockers.'], ['report.honest', 'Run Honest Mode.']);
546
630
  return cases.map(([id, title]) => ({ id, title, status: 'pending', evidence: [] }));
547
631
  }
632
+ export function buildQaLoopVisualEvidenceArtifact(mission = {}, contract = {}) {
633
+ const answers = contract.answers || {};
634
+ const uiRequired = qaUiRequired(answers);
635
+ const gptImage2ReviewRequired = qaGptImage2AnnotatedReviewRequired(contract, contract.prompt || mission.prompt);
636
+ return {
637
+ schema: 'sks.qa-loop-visual-evidence.v1',
638
+ generated_at: nowIso(),
639
+ mission_id: mission.id || contract.mission_id || null,
640
+ contract_hash: contract.sealed_hash || null,
641
+ required: uiRequired || gptImage2ReviewRequired,
642
+ chrome_extension_screenshot: {
643
+ required: uiRequired,
644
+ status: uiRequired ? 'pending' : 'not_required',
645
+ evidence_source: CODEX_WEB_VERIFICATION_EVIDENCE_SOURCE,
646
+ artifact_path: null,
647
+ sha256: null,
648
+ width: null,
649
+ height: null,
650
+ privacy: 'local-only'
651
+ },
652
+ gpt_image_2_annotated_review: {
653
+ required: gptImage2ReviewRequired,
654
+ status: gptImage2ReviewRequired ? 'pending' : 'not_required',
655
+ model: gptImage2ReviewRequired ? 'gpt-image-2' : 'not_required',
656
+ provider: gptImage2ReviewRequired ? 'Codex App $imagegen' : 'not_required',
657
+ source_screenshot_artifact: null,
658
+ artifact_path: null,
659
+ sha256: null,
660
+ width: null,
661
+ height: null,
662
+ required_output: gptImage2ReviewRequired ? 'generated_annotated_review_image_with_numbered_callouts_severity_labels_and_visual_marks' : 'not_required',
663
+ docs_url: CODEX_APP_IMAGE_GENERATION_DOC_URL,
664
+ privacy: 'local-only'
665
+ },
666
+ blockers: uiRequired ? ['chrome_extension_screenshot_missing'] : [],
667
+ notes: [
668
+ 'QA-LOOP web visual evidence must be backed by real saved local image files.',
669
+ CODEX_WEB_VERIFICATION_POLICY,
670
+ CODEX_IMAGEGEN_REQUIRED_POLICY
671
+ ]
672
+ };
673
+ }
674
+ async function missingQaLoopVisualEvidence(dir, gate = {}) {
675
+ const visual = await readJson(path.join(dir, QA_LOOP_VISUAL_EVIDENCE_ARTIFACT), null);
676
+ const reasons = [];
677
+ const uiRequired = gate.ui_e2e_required === true;
678
+ if (uiRequired) {
679
+ const screenshot = visual?.chrome_extension_screenshot || {};
680
+ if (gate.ui_chrome_extension_screenshot_captured !== true && !positiveVisualStatus(screenshot.status, ['captured', 'attached', 'verified']))
681
+ reasons.push('ui_chrome_extension_screenshot_missing');
682
+ const screenshotPath = firstNonEmpty(gate.ui_chrome_extension_screenshot_artifact, gate.chrome_extension_screenshot_artifact, gate.ui_chrome_extension_screenshot?.path, gate.chrome_extension_screenshot?.path, screenshot.artifact_path, screenshot.path);
683
+ const screenshotSha = firstNonEmpty(gate.ui_chrome_extension_screenshot_sha256, gate.chrome_extension_screenshot_sha256, gate.ui_chrome_extension_screenshot?.sha256, gate.chrome_extension_screenshot?.sha256, screenshot.sha256);
684
+ const screenshotDims = {
685
+ width: firstNonEmpty(gate.ui_chrome_extension_screenshot_width, gate.ui_chrome_extension_screenshot?.width, gate.chrome_extension_screenshot?.width, screenshot.width),
686
+ height: firstNonEmpty(gate.ui_chrome_extension_screenshot_height, gate.ui_chrome_extension_screenshot?.height, gate.chrome_extension_screenshot?.height, screenshot.height)
687
+ };
688
+ if (!screenshotPath)
689
+ reasons.push('ui_chrome_extension_screenshot_artifact_missing');
690
+ else
691
+ reasons.push(...await imageEvidenceFileReasons(dir, screenshotPath, screenshotSha, 'ui_chrome_extension_screenshot', screenshotDims));
692
+ const screenshotSource = firstNonEmpty(gate.ui_chrome_extension_screenshot_source, screenshot.evidence_source, gate.ui_evidence_source);
693
+ if (screenshotSource !== CODEX_WEB_VERIFICATION_EVIDENCE_SOURCE)
694
+ reasons.push('ui_chrome_extension_screenshot_source_not_codex_chrome_extension');
695
+ }
696
+ const review = visual?.gpt_image_2_annotated_review || {};
697
+ const gptImage2ReviewRequired = gate.gpt_image_2_annotated_review_required === true || review.required === true;
698
+ if (gptImage2ReviewRequired) {
699
+ if (gate.gpt_image_2_annotated_review_generated !== true && !positiveVisualStatus(review.status, ['generated', 'attached', 'verified']))
700
+ reasons.push('gpt_image_2_annotated_review_image_missing');
701
+ const reviewPath = firstNonEmpty(gate.gpt_image_2_annotated_review_artifact, gate.imagegen_annotated_review_artifact, gate.gpt_image_2_annotated_review?.path, gate.gpt_image_2_annotated_review_image?.path, review.artifact_path, review.path);
702
+ const reviewSha = firstNonEmpty(gate.gpt_image_2_annotated_review_sha256, gate.gpt_image_2_annotated_review?.sha256, gate.gpt_image_2_annotated_review_image?.sha256, review.sha256);
703
+ const reviewDims = {
704
+ width: firstNonEmpty(gate.gpt_image_2_annotated_review_width, gate.gpt_image_2_annotated_review?.width, gate.gpt_image_2_annotated_review_image?.width, review.width),
705
+ height: firstNonEmpty(gate.gpt_image_2_annotated_review_height, gate.gpt_image_2_annotated_review?.height, gate.gpt_image_2_annotated_review_image?.height, review.height)
706
+ };
707
+ if (!reviewPath)
708
+ reasons.push('gpt_image_2_annotated_review_artifact_missing');
709
+ else
710
+ reasons.push(...await imageEvidenceFileReasons(dir, reviewPath, reviewSha, 'gpt_image_2_annotated_review', reviewDims));
711
+ const model = firstNonEmpty(gate.gpt_image_2_annotated_review_model, gate.gpt_image_2_annotated_review?.model, gate.gpt_image_2_annotated_review_image?.model, review.model, review.provider?.model);
712
+ if (model !== 'gpt-image-2')
713
+ reasons.push('gpt_image_2_annotated_review_model_missing');
714
+ const provider = firstNonEmpty(gate.gpt_image_2_annotated_review_provider, gate.gpt_image_2_annotated_review?.provider, gate.gpt_image_2_annotated_review_image?.provider, review.provider, review.provider_surface);
715
+ if (!provider || !/codex\s+app|\$imagegen|codex_app_imagegen/i.test(String(provider)))
716
+ reasons.push('gpt_image_2_annotated_review_provider_not_codex_app_imagegen');
717
+ if (/mock|fake|fixture|placeholder|text[-_ ]?only|direct\s+api|openai_images_api|responses_image_generation/i.test(String(provider)))
718
+ reasons.push('gpt_image_2_annotated_review_provider_forbidden');
719
+ const sourceScreenshot = firstNonEmpty(gate.gpt_image_2_source_screenshot_artifact, gate.gpt_image_2_annotated_review?.source_screenshot_artifact, gate.gpt_image_2_annotated_review_image?.source_screenshot_artifact, review.source_screenshot_artifact, gate.ui_chrome_extension_screenshot_artifact);
720
+ if (!sourceScreenshot)
721
+ reasons.push('gpt_image_2_source_screenshot_artifact_missing');
722
+ }
723
+ return [...new Set(reasons)];
724
+ }
725
+ function positiveVisualStatus(status, accepted) {
726
+ return accepted.includes(String(status || '').trim().toLowerCase());
727
+ }
728
+ function firstNonEmpty(...values) {
729
+ for (const value of values) {
730
+ if (typeof value === 'string' && value.trim())
731
+ return value.trim();
732
+ if (value && typeof value !== 'string')
733
+ return value;
734
+ }
735
+ return null;
736
+ }
737
+ async function imageEvidenceFileReasons(dir, artifactPath, declaredSha, prefix, declaredDims = {}) {
738
+ const reasons = [];
739
+ const resolved = resolveEvidencePath(dir, artifactPath);
740
+ if (!resolved)
741
+ return [`${prefix}_artifact_path_invalid`];
742
+ if (!IMAGE_FILE_RE.test(resolved))
743
+ reasons.push(`${prefix}_artifact_not_image_file`);
744
+ if (!(await exists(resolved)))
745
+ return [...reasons, `${prefix}_artifact_file_missing`];
746
+ const sha = await sha256File(resolved).catch(() => null);
747
+ if (!declaredSha)
748
+ reasons.push(`${prefix}_sha256_missing`);
749
+ else if (sha && String(declaredSha) !== sha)
750
+ reasons.push(`${prefix}_sha256_mismatch`);
751
+ const dims = await imageDimensions(resolved).catch(() => null);
752
+ const width = Number(dims?.width ?? declaredDims?.width);
753
+ const height = Number(dims?.height ?? declaredDims?.height);
754
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
755
+ reasons.push(`${prefix}_dimensions_missing`);
756
+ return reasons;
757
+ }
758
+ function resolveEvidencePath(dir, artifactPath) {
759
+ const value = String(artifactPath || '').trim().replace(/^file:\/\//i, '');
760
+ if (!value || /^https?:\/\//i.test(value))
761
+ return null;
762
+ return path.isAbsolute(value) ? value : path.resolve(dir, value);
763
+ }
548
764
  function missionRootFromDir(dir) {
549
765
  const normalized = path.resolve(String(dir || ''));
550
766
  const marker = `${path.sep}.sneakoscope${path.sep}missions${path.sep}`;
@@ -564,7 +780,7 @@ function compactExecutionProfile(profile) {
564
780
  }
565
781
  function qaReportTemplate(mission, contract, checklist) {
566
782
  const a = contract.answers || {};
567
- return `# QA-LOOP Report\n\nMission: ${mission.id}\nTarget: ${a.TARGET_BASE_URL || 'unset'}\nScope: ${a.QA_SCOPE || 'unset'}\nEnvironment: ${a.TARGET_ENVIRONMENT || 'unset'}\n\n## Safety\n\n- Deployed destructive tests: never\n- Credentials: temp-only, never saved\n- UI evidence: ${CODEX_WEB_VERIFICATION_POLICY}\n\n## Checklist\n\n${checklist.map((item) => `- [ ] ${item.id}: ${item.title}`).join('\n')}\n\n## Findings\n\nTBD\n\n## Corrections And Rechecks\n\nTBD\n\n## Honest Mode\n\nTBD\n`;
783
+ return `# QA-LOOP Report\n\nMission: ${mission.id}\nTarget: ${a.TARGET_BASE_URL || 'unset'}\nScope: ${a.QA_SCOPE || 'unset'}\nEnvironment: ${a.TARGET_ENVIRONMENT || 'unset'}\n\n## Safety\n\n- Deployed destructive tests: never\n- Credentials: temp-only, never saved\n- UI evidence: ${CODEX_WEB_VERIFICATION_POLICY}\n- Visual evidence ledger: ${QA_LOOP_VISUAL_EVIDENCE_ARTIFACT}\n\n## Checklist\n\n${checklist.map((item) => `- [ ] ${item.id}: ${item.title}`).join('\n')}\n\n## Findings\n\nTBD\n\n## Corrections And Rechecks\n\nTBD\n\n## Honest Mode\n\nTBD\n`;
568
784
  }
569
785
  function positiveCount(value) {
570
786
  const n = Number(value || 0);
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
- import { writeJsonAtomic, writeTextAtomic } from './fsx.js';
2
+ import { nowIso, sha256, writeJsonAtomic, writeTextAtomic } from './fsx.js';
3
3
  import { buildQaLoopQuestionSchema } from './qa-loop.js';
4
4
  import { CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_WEB_VERIFICATION_POLICY, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, hasFromChatImgSignal } from './routes.js';
5
+ export const REQUEST_INTAKE_ARTIFACT = 'request-intake.json';
5
6
  export function buildQuestionSchemaForRoute(route, prompt) {
6
7
  if (String(route?.id || '') === 'QALoop')
7
8
  return buildQaLoopQuestionSchema(prompt);
@@ -222,6 +223,8 @@ export function inferAnswersForPrompt(prompt, explicitAnswers = {}) {
222
223
  const versionWork = /버전|version|bump|release|publish:dry|npm\s+pack/.test(lower);
223
224
  const installWork = /bootstrap|postinstall|doctor|deps|tmux|homebrew|first install|최초\s*설치|설치\s*ux|셋업|setup/.test(lower);
224
225
  const questionGateWork = /모호|ambiguity|clarification|질문|triwiki|추론|infer|predict|예측|answers?\.json|decision-contract/.test(lower);
226
+ const requestIntakeWork = /(모호|ambiguity|ambiguous|vague|rough|의도|intent|요청사항|requirements?|프롬프트|prompt|triwiki|위키|wiki)/.test(lower)
227
+ && /(파이프라인|pipeline|변환|transform|rewrite|compile|리스트|list|누락|missing|의도|intent|request[- ]?intake|intake)/.test(lower);
225
228
  const uiuxWork = /\b(ui|modal|screen|button|visual|design|layout|component|prototype|frontend)\b|화면|버튼|모달|디자인|레이아웃|컴포넌트|프론트|시각|발표자료|디자인\s*시스템/.test(lower);
226
229
  const presentationWork = looksLikePresentationArtifactPrompt(lower);
227
230
  const dbWork = new RegExp(["\\bdb\\b", "database", "schema", "migration", "tab" + "le", "col" + "umn", "rls", "supabase", "postgres", "sql", "테이블", "마이그레이션", "스키마", "컬럼", "열", "행", "데이터베이스"].join("|")).test(lower);
@@ -247,12 +250,13 @@ export function inferAnswersForPrompt(prompt, explicitAnswers = {}) {
247
250
  && !versionWork
248
251
  && !presentationWork
249
252
  && !chatCaptureWork;
250
- const kind = versionWork ? 'version' : chatCaptureWork ? 'chat_capture' : triwikiAuditWork ? 'triwiki_audit' : effectivePrioritySignalWork ? 'priority' : questionGateWork ? 'questions' : installWork ? 'install' : null;
253
+ const kind = versionWork ? 'version' : chatCaptureWork ? 'chat_capture' : triwikiAuditWork ? 'triwiki_audit' : effectivePrioritySignalWork ? 'priority' : requestIntakeWork ? 'request_intake' : questionGateWork ? 'questions' : installWork ? 'install' : null;
251
254
  const goals = {
252
255
  version: version ? `sneakoscope 버전을 ${version}로 올린다` : 'sneakoscope 버전을 다음 patch 버전으로 올린다',
253
256
  chat_capture: 'From-Chat-IMG로 채팅 요구사항과 첨부 원본 이미지를 매칭해 고객사 작업 지시서를 만들고 반영한다',
254
257
  triwiki_audit: 'TriWiki가 반복 실수를 막는지 검수하고, 실패 경로를 코드와 검증으로 개선한다',
255
258
  priority: '강한 불만과 반복 요청을 TriWiki 우선순위 신호로 기록한다',
259
+ request_intake: '모호한 사용자 요청을 TriWiki 기반 request-intake로 해석하고, 누락 없는 요구사항 목록과 실행용 변환 프롬프트를 pipeline에 전달한다',
256
260
  questions: '예측 가능한 답은 추론하고 실제 모호한 항목만 질문한다',
257
261
  presentation: '청중과 STP 전략에 맞는 HTML 기반 발표자료/PDF 산출물을 만든다',
258
262
  install: 'SKS 최초 설치와 bootstrap을 한 번에 준비 상태까지 연결한다'
@@ -262,6 +266,7 @@ export function inferAnswersForPrompt(prompt, explicitAnswers = {}) {
262
266
  chat_capture: ['From-Chat-IMG activates chat-image intake only here', 'all visible chat requirements are listed before implementation', `${FROM_CHAT_IMG_COVERAGE_ARTIFACT} maps every customer request, screenshot region, and attachment to work-order item(s)`, `${FROM_CHAT_IMG_CHECKLIST_ARTIFACT} is updated as each request, image match, work item, scoped QA-LOOP, and verification step is completed`, `${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT} records temporary TriWiki-backed session context with retention metadata`, `${FROM_CHAT_IMG_QA_LOOP_ARTIFACT} proves QA-LOOP ran over the exact customer-request work-order range after implementation`, 'unresolved_items is empty before Team completion', 'scoped_qa_loop_completed is true with zero unresolved QA findings', 'Web/browser visual inspection uses Codex Chrome Extension readiness first; native Mac/non-web visual inspection uses Codex Computer Use when available', CODEX_WEB_VERIFICATION_POLICY, CODEX_COMPUTER_USE_ONLY_POLICY, 'client requests follow normal SKS gates and verification'],
263
267
  triwiki_audit: ['TriWiki ingestion, voxel attention, and contract consumption paths are inspected against current code', 'repeat-mistake prevention gaps are fixed in the relevant code path or blocked with evidence', 'regression coverage proves fresh/high-weight mistake memory can influence future missions', 'final status separates supported behavior from anything still unverified'],
264
268
  priority: ['strong feedback raises required_weight', 'request topics are counted in wiki packs', 'future inference uses priority signals'],
269
+ request_intake: ['request-intake.json records original prompt, interpreted intent, source-order requirements, wiki context used, and transformed prompt', 'pipeline-plan.json attaches request_intake metadata and execution_prompt from request-intake.transformed_prompt', 'decision-contract.json preserves the same request-intake artifact when a contract is sealed'],
265
270
  questions: ['predictable answers are inferred', 'partial answers can seal contracts', 'only unresolved changing slots remain visible'],
266
271
  presentation: ['audience profile and STP strategy are explicit before artifact creation', 'target pain points map to proposed solution moments', 'decision context and likely objections are sealed before storyboarding', 'presentation format, device, and delivery context are fixed before design work'],
267
272
  install: ['bootstrap/deps initialize readiness', 'missing runtime deps show repair actions', 'readiness output is concrete']
@@ -381,6 +386,222 @@ export function inferAnswersForPrompt(prompt, explicitAnswers = {}) {
381
386
  }
382
387
  return { answers: inferred, notes };
383
388
  }
389
+ export function buildRequestIntake(prompt, explicitAnswers = {}, opts = {}) {
390
+ const originalPrompt = String(prompt || '').trim();
391
+ const inferred = inferAnswersForPrompt(originalPrompt, explicitAnswers);
392
+ const answers = { ...(inferred.answers || {}), ...(explicitAnswers || {}) };
393
+ const ambiguity = buildAmbiguityAssessment(originalPrompt, explicitAnswers);
394
+ const wikiContext = summarizeWikiContext(opts.wikiContext);
395
+ const promptRequirements = promptRequirementItems(originalPrompt);
396
+ const semanticRequirements = semanticRequirementItems(originalPrompt);
397
+ const acceptance = toStringList(answers.ACCEPTANCE_CRITERIA);
398
+ const constraints = toStringList(answers.RISK_BOUNDARY);
399
+ const nonGoals = toStringList(answers.NON_GOALS);
400
+ const requirements = dedupeRequirementItems([
401
+ ...promptRequirements,
402
+ ...semanticRequirements,
403
+ ...acceptance.map((text, index) => requirementItem('acceptance', index + 1, text, 'inferred_acceptance_criteria')),
404
+ ...constraints.map((text, index) => requirementItem('constraint', index + 1, text, 'inferred_safety_constraint'))
405
+ ]);
406
+ const goal = String(answers.GOAL_PRECISE || originalPrompt || '사용자 요청을 현재 코드 기준으로 구현한다').trim();
407
+ const targetSignals = extractTargetSignals(originalPrompt);
408
+ const riskSignals = extractRiskSignals(originalPrompt, constraints);
409
+ const transformedPrompt = buildTransformedPrompt({
410
+ originalPrompt,
411
+ goal,
412
+ wikiContext,
413
+ requirements,
414
+ constraints,
415
+ acceptance,
416
+ nonGoals,
417
+ targetSignals,
418
+ riskSignals,
419
+ ambiguity
420
+ });
421
+ return {
422
+ schema: 'sks.request-intake.v1',
423
+ generated_at: nowIso(),
424
+ prompt_hash: sha256(originalPrompt).slice(0, 16),
425
+ original_prompt: originalPrompt,
426
+ interpreted_intent: {
427
+ goal,
428
+ source: 'prompt_plus_triwiki_current_code_defaults',
429
+ confidence: ambiguity.ready_for_contract ? 'high' : ambiguity.overall_score <= 0.35 ? 'medium' : 'needs_human_only_if_scope_changes'
430
+ },
431
+ ambiguity_assessment: ambiguity,
432
+ wiki_context_used: wikiContext,
433
+ request_items: requirements,
434
+ requirements,
435
+ constraints,
436
+ acceptance_criteria: acceptance,
437
+ non_goals: nonGoals,
438
+ target_signals: targetSignals,
439
+ risk_signals: riskSignals,
440
+ transformed_prompt: transformedPrompt,
441
+ pipeline_usage: {
442
+ artifact: REQUEST_INTAKE_ARTIFACT,
443
+ read_before_pipeline_stage: true,
444
+ use_transformed_prompt_for_execution: true,
445
+ preserve_original_prompt: true,
446
+ ask_user_only_for_scope_safety_behavior_or_acceptance_changes: true
447
+ }
448
+ };
449
+ }
450
+ function summarizeWikiContext(wikiContext = null) {
451
+ const claims = Array.isArray(wikiContext?.claims) ? wikiContext.claims : [];
452
+ const useFirstIds = new Set((wikiContext?.attention?.use_first || []).map((row) => Array.isArray(row) ? row[0] : row?.id).filter(Boolean));
453
+ const hydrateFirstIds = new Set((wikiContext?.attention?.hydrate_first || []).map((row) => Array.isArray(row) ? row[0] : row?.id).filter(Boolean));
454
+ return {
455
+ source: wikiContext ? '.sneakoscope/wiki/context-pack.json' : 'unavailable',
456
+ attention_mode: wikiContext?.attention?.mode || null,
457
+ use_first_ids: [...useFirstIds].slice(0, 8),
458
+ hydrate_first_ids: [...hydrateFirstIds].slice(0, 8),
459
+ claims: claims.slice(0, 8).map((claim) => ({
460
+ id: claim.id || claim.claim_id || null,
461
+ trust: claim.trust_score || claim.trust || null,
462
+ source: claim.source || claim.source_path || null,
463
+ summary: String(claim.claim || claim.summary || claim.text || '').slice(0, 320)
464
+ }))
465
+ };
466
+ }
467
+ function promptRequirementItems(prompt) {
468
+ const cleaned = prompt
469
+ .replace(/(?:^|\s)\$[A-Za-z0-9_-]+(?:\s|$)/g, ' ')
470
+ .replace(/\s+/g, ' ')
471
+ .trim();
472
+ if (!cleaned)
473
+ return [];
474
+ const sourceChunks = cleaned
475
+ .split(/\n+|(?:^|\s)[-*]\s+/)
476
+ .flatMap((chunk) => splitLooseClauses(chunk))
477
+ .map((chunk) => chunk.trim())
478
+ .filter(Boolean);
479
+ const chunks = sourceChunks.length ? sourceChunks : [cleaned];
480
+ return chunks.slice(0, 12).map((text, index) => requirementItem('prompt', index + 1, text, 'source_prompt_order'));
481
+ }
482
+ function splitLooseClauses(text) {
483
+ const compact = String(text || '').trim();
484
+ if (compact.length < 80)
485
+ return [compact].filter(Boolean);
486
+ const pieces = compact.split(/\s+(?=(?:그리고|또|또한|그걸로|해당|이거는|이건|너가|우리|다음|then|and)\b)/i);
487
+ return pieces.length > 1 ? pieces : [compact];
488
+ }
489
+ function semanticRequirementItems(prompt) {
490
+ const text = String(prompt || '');
491
+ const lower = text.toLowerCase();
492
+ const items = [];
493
+ const add = (text, confidence = 0.88) => items.push({
494
+ id: `REQ-S${String(items.length + 1).padStart(2, '0')}`,
495
+ kind: 'semantic_requirement',
496
+ source: 'semantic_prompt_signal',
497
+ text,
498
+ required: true,
499
+ confidence
500
+ });
501
+ if (/모호|ambiguous|rough|대충|애매/.test(lower))
502
+ add('모호한 사용자 입력을 그대로 실행하지 말고 intent intake 단계에서 명확한 실행 요청으로 해석한다.');
503
+ if (/위키|triwiki|wiki|memory|기억/.test(lower))
504
+ add('TriWiki/context-pack과 현재 코드 기본값을 참고해 사용자 의도와 반복 선호를 보강한다.');
505
+ if (/의도|intent|목적/.test(lower) && /명확|파악|extract|understand/.test(lower))
506
+ add('사용자 의도를 Goal, Context, Constraints, Done-when 형태로 명확히 정리한다.');
507
+ if (/리스트|list|누락|빠짐|requirements?|요청사항/.test(lower))
508
+ add('요청사항을 source order 기준으로 누락 없이 리스트업하고 각 항목을 실행 작업으로 매핑한다.');
509
+ if (/프롬프트|prompt/.test(lower) && /변환|바꿔|rewrite|transform|compile|최적/.test(lower))
510
+ add('원문 요청을 파이프라인 실행에 적합한 transformed prompt로 변환한다.');
511
+ if (/파이프라인|pipeline/.test(lower) && /(태워|전달|route|run|execute|투입)/.test(lower))
512
+ add('변환된 prompt와 request-intake artifact를 실제 route pipeline에 전달한다.');
513
+ return items;
514
+ }
515
+ function requirementItem(kind, index, text, source) {
516
+ return {
517
+ id: `REQ-${String(index).padStart(3, '0')}`,
518
+ kind,
519
+ source,
520
+ text: String(text || '').trim(),
521
+ required: true,
522
+ confidence: source === 'source_prompt_order' ? 1 : 0.86
523
+ };
524
+ }
525
+ function dedupeRequirementItems(items = []) {
526
+ const seen = new Set();
527
+ const out = [];
528
+ for (const item of items) {
529
+ const text = String(item.text || '').replace(/\s+/g, ' ').trim();
530
+ if (!text)
531
+ continue;
532
+ const key = text.toLowerCase();
533
+ if (seen.has(key))
534
+ continue;
535
+ seen.add(key);
536
+ out.push({ ...item, id: `REQ-${String(out.length + 1).padStart(3, '0')}`, text });
537
+ }
538
+ return out;
539
+ }
540
+ function toStringList(value) {
541
+ if (Array.isArray(value))
542
+ return value.map((item) => String(item || '').trim()).filter(Boolean);
543
+ const text = String(value || '').trim();
544
+ if (!text)
545
+ return [];
546
+ return text.split(/\n+/).map((item) => item.trim()).filter(Boolean);
547
+ }
548
+ function extractTargetSignals(prompt) {
549
+ const text = String(prompt || '');
550
+ const paths = [...text.matchAll(/(?:src|test|tests|scripts|docs|README|CHANGELOG|package\.json|\.sneakoscope|\.agents|\.codex)\/?[A-Za-z0-9._/\-]*/g)].map((m) => m[0]);
551
+ const commands = [...text.matchAll(/\$[A-Za-z0-9_-]+/g)].map((m) => m[0]);
552
+ const urls = [...text.matchAll(/https?:\/\/[^\s)\]}>,]+/g)].map((m) => m[0]);
553
+ return {
554
+ paths: [...new Set(paths)].slice(0, 12),
555
+ dollar_commands: [...new Set(commands)].slice(0, 12),
556
+ urls: [...new Set(urls)].slice(0, 8)
557
+ };
558
+ }
559
+ function extractRiskSignals(prompt, constraints = []) {
560
+ const lower = String(prompt || '').toLowerCase();
561
+ const risks = [];
562
+ if (promptHasRisk(lower))
563
+ risks.push('high_risk_surface_detected');
564
+ if (promptNeedsExplicitRiskBoundary(lower))
565
+ risks.push('explicit_risk_boundary_required');
566
+ if (/db|database|supabase|postgres|sql|schema|migration|데이터베이스|스키마|마이그레이션/.test(lower))
567
+ risks.push('database_safety');
568
+ if (/browser|webapp|localhost|chrome|스크린샷|화면|ui|ux|visual|웹/.test(lower))
569
+ risks.push('visual_or_browser_evidence');
570
+ if (/publish|release|version|배포|릴리즈|버전/.test(lower))
571
+ risks.push('release_surface');
572
+ return {
573
+ risks: [...new Set(risks)],
574
+ constraints
575
+ };
576
+ }
577
+ function buildTransformedPrompt(input) {
578
+ const requirementLines = (input.requirements || []).map((item, index) => `${index + 1}. ${item.text}`);
579
+ const wikiLines = (input.wikiContext?.claims || []).slice(0, 5).map((claim) => `- ${claim.id || 'wiki-claim'}: ${claim.summary}`);
580
+ return [
581
+ '# SKS Wiki-Informed Execution Prompt',
582
+ '',
583
+ '## Goal',
584
+ input.goal,
585
+ '',
586
+ '## Original Prompt',
587
+ input.originalPrompt || '(empty)',
588
+ '',
589
+ '## Wiki Context To Use First',
590
+ ...(wikiLines.length ? wikiLines : ['- No current TriWiki claims were available; rely on current code and conservative SKS defaults.']),
591
+ '',
592
+ '## Requirements',
593
+ ...(requirementLines.length ? requirementLines : ['1. Implement the user request using current code context.']),
594
+ '',
595
+ '## Constraints',
596
+ ...((input.constraints || []).length ? input.constraints.map((row) => `- ${row}`) : ['- Preserve existing behavior unless the request explicitly changes it.', '- Do not create unrequested fallback implementation code.', '- Do not run destructive or live-data mutation commands.']),
597
+ '',
598
+ '## Done When',
599
+ ...((input.acceptance || []).length ? input.acceptance.map((row) => `- ${row}`) : ['- Relevant implementation is complete.', '- Focused verification passes or unavailable checks are explicitly justified.', '- Final response states changed, verified, and unverified items.']),
600
+ '',
601
+ '## Pipeline Instruction',
602
+ 'Read request-intake.json first, execute this transformed prompt through the selected SKS route, refresh/validate TriWiki after findings or artifact changes, and finish with SKS Honest Mode.'
603
+ ].join('\n');
604
+ }
384
605
  export function buildQuestionSchema(prompt) {
385
606
  const lower = String(prompt || '').toLowerCase();
386
607
  const domainHints = [];
@@ -438,6 +659,7 @@ export function buildQuestionSchema(prompt) {
438
659
  slots.push({ id: 'DB_MIGRATION_APPLY_ALLOWED', question: 'migration 적용이 필요할 경우 어디까지 허용하나요?', required: true, type: 'enum', options: ['no', 'local_only', 'preview_branch_only'] }, { id: 'DB_READ_ONLY_QUERY_LIMIT', question: 'MCP/SQL read-only 조회 시 기본 LIMIT를 몇으로 둘까요?', required: true, type: 'string' });
439
660
  }
440
661
  const inferred = inferAnswersForPrompt(prompt);
662
+ const requestIntake = buildRequestIntake(prompt, inferred.answers);
441
663
  const inferredSlots = new Set(Object.keys(inferred.answers));
442
664
  const askedSlots = [];
443
665
  return {
@@ -446,6 +668,7 @@ export function buildQuestionSchema(prompt) {
446
668
  prompt,
447
669
  domain_hints: domainHints,
448
670
  ambiguity_assessment: ambiguity,
671
+ request_intake: requestIntake,
449
672
  inferred_answers: inferred.answers,
450
673
  inference_notes: inferred.notes,
451
674
  slots: askedSlots
@@ -478,6 +701,20 @@ export function questionsMarkdown(schema) {
478
701
  lines.push(`- unresolved dimensions: ${(schema.ambiguity_assessment.unresolved_dimensions || []).join(', ') || 'none'}`);
479
702
  lines.push(`- legacy question budget ignored: ${schema.ambiguity_assessment.question_budget}`);
480
703
  }
704
+ if (schema.request_intake) {
705
+ const intake = schema.request_intake;
706
+ lines.push('');
707
+ lines.push('## Request Intake');
708
+ lines.push('');
709
+ lines.push(`- artifact: ${REQUEST_INTAKE_ARTIFACT}`);
710
+ lines.push(`- interpreted goal: ${intake.interpreted_intent?.goal || '(none)'}`);
711
+ lines.push(`- requirement count: ${(intake.requirements || []).length}`);
712
+ lines.push(`- wiki context: ${intake.wiki_context_used?.source || 'unavailable'}`);
713
+ lines.push('');
714
+ lines.push('```markdown');
715
+ lines.push(intake.transformed_prompt || '');
716
+ lines.push('```');
717
+ }
481
718
  if (schema.inferred_answers && Object.keys(schema.inferred_answers).length) {
482
719
  lines.push('');
483
720
  lines.push('## Inferred Answers');