sneakoscope 0.7.49 → 0.7.51

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.
@@ -14,11 +14,13 @@ import { MISTAKE_RECALL_ARTIFACT, mistakeRecallGateStatus } from './mistake-reca
14
14
  import { recordSkillDreamEvent, skillDreamPolicyText, writeSkillForgeReport } from './skill-forge.mjs';
15
15
  import { writeResearchPlan } from './research.mjs';
16
16
  import { PPT_REQUIRED_GATE_FIELDS } from './ppt.mjs';
17
+ import { IMAGE_UX_REVIEW_GATE_ARTIFACT, IMAGE_UX_REVIEW_POLICY_ARTIFACT, IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT, IMAGE_UX_REVIEW_REQUIRED_GATE_FIELDS, writeImageUxReviewRouteArtifacts } from './image-ux-review.mjs';
17
18
  import { SPEED_LANE_POLICY } from './proof-field.mjs';
18
19
  import { permissionGateSummary } from './permission-gates.mjs';
19
- import { CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, chatCaptureIntakeText, context7RequirementText, dollarCommand, evidenceMentionsForbiddenBrowserAutomation, getdesignReferencePolicyText, hasFromChatImgSignal, hasMadSksSignal, noUnrequestedFallbackCodePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, reflectionRequiredForRoute, reasoningInstruction, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stripDollarCommand, stripMadSksSignal, subagentExecutionPolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
20
+ import { CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, chatCaptureIntakeText, context7RequirementText, dollarCommand, evidenceMentionsForbiddenBrowserAutomation, getdesignReferencePolicyText, hasFromChatImgSignal, hasMadSksSignal, imageUxReviewPipelinePolicyText, noUnrequestedFallbackCodePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, reflectionRequiredForRoute, reasoningInstruction, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stripDollarCommand, stripMadSksSignal, subagentExecutionPolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
20
21
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from './team-dag.mjs';
21
22
  import { formatRoleCounts, initTeamLive, parseTeamSpecText } from './team-live.mjs';
23
+ import { evaluateTeamReviewPolicyGate, MIN_TEAM_REVIEWER_LANES, MIN_TEAM_REVIEW_POLICY_TEXT, teamReviewPolicy } from './team-review-policy.mjs';
22
24
 
23
25
  export { routePrompt };
24
26
 
@@ -281,9 +283,10 @@ export function promptPipelineContext(prompt, route = routePrompt(prompt)) {
281
283
  'Before final answer, include a user-visible completion summary that explains what changed and how it was verified, then run SKS Honest Mode: verify evidence/tests, state gaps, and confirm the goal is genuinely complete.'
282
284
  ];
283
285
  if (reflectionRequiredForRoute(route)) lines.push(reflectionInstructionText());
284
- if (route?.id === 'Team') lines.push(`Team route: scouts, TriWiki refresh, debate, consensus, runtime graph compile with concrete task ids and worker inboxes, close planning agents, fresh executors, review/integration, ${TEAM_SESSION_CLEANUP_ARTIFACT}, reflection, and Honest Mode.`);
286
+ if (route?.id === 'Team') lines.push(`Team route: scouts, TriWiki refresh, debate, consensus, runtime graph compile with concrete task ids and worker inboxes, close planning agents, fresh executors, minimum ${MIN_TEAM_REVIEWER_LANES}-lane review/integration, ${TEAM_SESSION_CLEANUP_ARTIFACT}, reflection, and Honest Mode. ${MIN_TEAM_REVIEW_POLICY_TEXT}`);
285
287
  if (route?.id === 'Goal') lines.push('Goal route: write SKS goal bridge artifacts, then use Codex native /goal persistence for create, pause, resume, and clear continuation controls.');
286
288
  if (route?.id === 'PPT') lines.push(`PPT route: before design or PDF work, seal delivery context, audience profile including average age/job/industry, STP strategy, decision context, and at least three pain-point to solution mappings. Keep the visual system simple, restrained, and information-first; design detail should come from hierarchy, spacing, alignment, rules, and subtle accents rather than decorative overdesign. ${pptPipelineAllowlistPolicyText()} If generated image assets or slide visual critique are needed, use gpt-image-2/imagegen only when that asset/review need is explicitly sealed in the $PPT contract, preferring Codex App built-in image generation (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) and using the OpenAI Image API when OPENAI_API_KEY is available; do not fabricate image files or image-review results when gpt-image-2 is unavailable. Then build source ledger, fact ledger, image asset ledger, storyboard with aha moments, style tokens, editable source HTML under source-html/, PDF artifact, render QA, bounded review ledger/iteration report, PPT-only temporary build file cleanup, and ppt-parallel-report.json so independent strategy/render/file-write phases stay parallel-friendly, then reflection and Honest Mode.`);
289
+ if (route?.id === 'ImageUXReview') lines.push(`Image UX Review route: ${imageUxReviewPipelinePolicyText()} Use ${IMAGE_UX_REVIEW_POLICY_ARTIFACT}, ${IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT}, ${IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT}, ${IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT}, ${IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT}, and ${IMAGE_UX_REVIEW_GATE_ARTIFACT} as the route evidence set. The route may suggest safe fixes only when the user requested fixing; otherwise report findings and blockers.`);
287
290
  if (route?.id === 'AutoResearch') lines.push('AutoResearch route: load autoresearch-loop plus seo-geo-optimizer when SEO/GEO, discoverability, README, npm, GitHub stars, ranking, or AI-search visibility is relevant.');
288
291
  if (route?.id === 'DB') lines.push('DB route: scan/check database risk first; destructive DB operations remain forbidden.');
289
292
  if (route?.id === 'GX') lines.push('GX route: use deterministic vgraph/beta render, validate, drift, and snapshot artifacts.');
@@ -336,6 +339,7 @@ export async function prepareRoute(root, prompt, state = {}) {
336
339
  if (route.id === 'ComputerUse') return withSkillDreamContext(await prepareComputerUseFastRoute(route, task), dreamContext);
337
340
  if (route.id === 'Wiki') return withSkillDreamContext(await prepareWikiQuickRoute(route, task), dreamContext);
338
341
  if (route.id === 'Goal') return withSkillDreamContext(await prepareGoal(root, route, task, routeNeedsContext7(route, prompt)), dreamContext);
342
+ if (route.id === 'ImageUXReview') return withSkillDreamContext(await prepareImageUxReview(root, route, task, routeNeedsContext7(route, prompt)), dreamContext);
339
343
  const required = routeNeedsContext7(route, prompt);
340
344
  const reasoning = routeReasoning(route, prompt);
341
345
  const subagentsRequired = routeRequiresSubagents(route, prompt);
@@ -428,6 +432,45 @@ async function prepareWikiQuickRoute(route, task) {
428
432
  };
429
433
  }
430
434
 
435
+ async function prepareImageUxReview(root, route, task, required) {
436
+ const { id, dir, mission } = await createMission(root, { mode: 'image-ux-review', prompt: task });
437
+ const contract = {
438
+ prompt: task,
439
+ answers: {
440
+ TARGET_SURFACE: task,
441
+ IMAGE_UX_REVIEW_SOURCE_IMAGES: []
442
+ },
443
+ sealed_hash: null,
444
+ mission_id: id
445
+ };
446
+ const artifacts = await writeImageUxReviewRouteArtifacts(dir, contract);
447
+ await writeJsonAtomic(path.join(dir, 'route-context.json'), {
448
+ route: route.id,
449
+ command: route.command,
450
+ mode: route.mode,
451
+ task,
452
+ mission_id: mission.id,
453
+ required_skills: route.requiredSkills,
454
+ context7_required: required,
455
+ context_tracking: triwikiContextTracking(),
456
+ stop_gate: route.stopGate,
457
+ artifact_policy: 'imagegen_generated_review_image_required_before_issue_extraction'
458
+ });
459
+ const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task, required, ambiguity: { required: false, status: 'direct_route' } });
460
+ await setCurrent(root, routeState(id, route, 'IMAGE_UX_REVIEW_READY', required, {
461
+ prompt: task,
462
+ implementation_allowed: true,
463
+ ambiguity_gate_required: false,
464
+ ambiguity_gate_passed: true,
465
+ stop_gate: route.stopGate,
466
+ image_ux_review_gate_ready: true,
467
+ image_ux_review_policy_ready: true,
468
+ pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok,
469
+ pipeline_plan_path: PIPELINE_PLAN_ARTIFACT
470
+ }));
471
+ return routeContext(route, id, task, required, `Capture or attach source UI screenshots, run Codex App $imagegen/gpt-image-2 to generate annotated review images, extract those generated images into ${IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT}, then update ${IMAGE_UX_REVIEW_GATE_ARTIFACT}. Initial gate blockers: ${(artifacts.gate.blockers || []).join(', ') || 'none'}.`);
472
+ }
473
+
431
474
  export async function activeRouteContext(root, state) {
432
475
  if (!state?.route && !state?.mode) return '';
433
476
  const id = state.route || state.mode;
@@ -448,7 +491,7 @@ export async function activeRouteContext(root, state) {
448
491
  ? ' Context7 evidence is still required before completion: use resolve-library-id, then query-docs (or legacy get-library-docs).'
449
492
  : '';
450
493
  const roles = state.role_counts ? ` Role counts: ${formatRoleCounts(state.role_counts)}.` : '';
451
- return `Active Team mission ${state.mission_id || 'latest'} must keep the user-visible live transcript updated. Agent session budget: ${state.agent_sessions || 3}.${roles} Run scouts, TriWiki refresh, debate, consensus, fresh development, review/integration, then close or account for every Team subagent session and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection/final. After each subagent status/result/handoff, run: sks team event ${state.mission_id || 'latest'} --agent <name> --phase <phase> --message "...". Inspect with sks team log/watch ${state.mission_id || 'latest'}.${reasoningNote}${context7}${planNote}`;
494
+ return `Active Team mission ${state.mission_id || 'latest'} must keep the user-visible live transcript updated. Agent session budget: ${state.agent_sessions || MIN_TEAM_REVIEWER_LANES}.${roles} Run scouts, TriWiki refresh, debate, consensus, fresh development, minimum ${MIN_TEAM_REVIEWER_LANES}-lane review/integration, then close or account for every Team subagent session and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection/final. ${MIN_TEAM_REVIEW_POLICY_TEXT} After each subagent status/result/handoff, run: sks team event ${state.mission_id || 'latest'} --agent <name> --phase <phase> --message "...". Inspect with sks team log/watch ${state.mission_id || 'latest'}.${reasoningNote}${context7}${planNote}`;
452
495
  }
453
496
  if (state.subagents_required && !(await hasSubagentEvidence(root, state))) {
454
497
  return `Active SKS route ${id} requires subagent execution evidence before code-changing work can be considered complete. Spawn worker/reviewer subagents for disjoint write scopes, or record an explicit unavailable/unsplittable subagent evidence event before editing.${reasoningNote}${planNote}`;
@@ -688,9 +731,11 @@ async function materializeAutoSealedTeam(root, id, dir, route, task, contractHas
688
731
  mission_id: id,
689
732
  task: cleanTask,
690
733
  agent_session_count: agentSessions,
691
- default_agent_session_count: 3,
734
+ default_agent_session_count: MIN_TEAM_REVIEWER_LANES,
692
735
  role_counts: roleCounts,
693
736
  session_policy: `Use at most ${agentSessions} subagent sessions at a time; the parent orchestrator is not counted.`,
737
+ review_policy: teamReviewPolicy(),
738
+ review_gate: evaluateTeamReviewPolicyGate({ roleCounts, agentSessions, roster }),
694
739
  bundle_size: roster.bundle_size,
695
740
  roster,
696
741
  contract_hash: contractHash,
@@ -698,7 +743,8 @@ async function materializeAutoSealedTeam(root, id, dir, route, task, contractHas
698
743
  phases: ['parallel_analysis_scouts', 'triwiki_stage_refresh', 'debate_team', 'runtime_task_graph', 'development_team', 'review'],
699
744
  analysis_team: `Read-only parallel scouting with exactly ${roster.bundle_size} analysis_scout_N agents.`,
700
745
  debate_team: `Read-only role debate with exactly ${roster.bundle_size} participants.`,
701
- development_team: `Fresh parallel development bundle with exactly ${roster.bundle_size} executor_N developers implementing disjoint slices.`
746
+ development_team: `Fresh parallel development bundle with exactly ${roster.bundle_size} executor_N developers implementing disjoint slices.`,
747
+ review_team: `Validation runs at least ${MIN_TEAM_REVIEWER_LANES} independent reviewer/QA lanes before integration or final.`
702
748
  },
703
749
  context_tracking: triwikiContextTracking(),
704
750
  team_runtime: teamRuntimePlanMetadata(),
@@ -709,7 +755,7 @@ async function materializeAutoSealedTeam(root, id, dir, route, task, contractHas
709
755
  { id: 'planning_debate', goal: 'Run read-only planning debate, map constraints and implementation slices, then seal one objective.', agents: roster.debate_team.map((agent) => agent.id) },
710
756
  { id: 'runtime_task_graph_compile', goal: `Compile the agreed Team plan into ${TEAM_GRAPH_ARTIFACT}, ${TEAM_RUNTIME_TASKS_ARTIFACT}, ${TEAM_DECOMPOSITION_ARTIFACT}, and ${TEAM_INBOX_DIR}.`, agents: ['parent_orchestrator'], output: [TEAM_GRAPH_ARTIFACT, TEAM_RUNTIME_TASKS_ARTIFACT, TEAM_DECOMPOSITION_ARTIFACT, TEAM_INBOX_DIR] },
711
757
  { id: 'parallel_implementation', goal: `Close debate agents, then spawn a fresh ${roster.bundle_size}-person executor development team with non-overlapping write ownership.`, agents: roster.development_team.map((agent) => agent.id) },
712
- { id: 'review_integration', goal: 'Review, integrate, verify, and record evidence before final.', agents: roster.validation_team.map((agent) => agent.id) },
758
+ { id: 'review_integration', goal: `${MIN_TEAM_REVIEW_POLICY_TEXT} Integrate, verify, and record evidence before final.`, agents: roster.validation_team.map((agent) => agent.id), min_reviewer_lanes: MIN_TEAM_REVIEWER_LANES },
713
759
  { id: 'session_cleanup', goal: `Close or account for Team subagent sessions and write ${TEAM_SESSION_CLEANUP_ARTIFACT}.`, agents: ['parent_orchestrator'] }
714
760
  ],
715
761
  live_visibility: {
@@ -724,7 +770,7 @@ async function materializeAutoSealedTeam(root, id, dir, route, task, contractHas
724
770
  await writeJsonAtomic(path.join(dir, 'team-plan.json'), plan);
725
771
  await writeJsonAtomic(path.join(dir, 'team-roster.json'), { schema_version: 1, mission_id: id, role_counts: roleCounts, agent_sessions: agentSessions, bundle_size: roster.bundle_size, roster, confirmed: true, source: 'auto_sealed_team_spec' });
726
772
  const contextTracking = triwikiContextTracking();
727
- await writeTextAtomic(path.join(dir, 'team-workflow.md'), `# SKS Team Workflow\n\nTask: ${cleanTask}\n\nAgent session budget: ${agentSessions}\nBundle size: ${roster.bundle_size}\nRole counts: ${formatRoleCounts(roleCounts)}\nContext tracking: ${contextTracking.ssot} SSOT, ${contextTracking.default_pack}.\n\nAuto-sealed ambiguity gate: no user question was required. Continue directly with Team scouting, debate, runtime task graph, implementation, review, cleanup, reflection, and Honest Mode.\n`);
773
+ await writeTextAtomic(path.join(dir, 'team-workflow.md'), `# SKS Team Workflow\n\nTask: ${cleanTask}\n\nAgent session budget: ${agentSessions}\nBundle size: ${roster.bundle_size}\nRole counts: ${formatRoleCounts(roleCounts)}\nReview policy: ${MIN_TEAM_REVIEW_POLICY_TEXT}\nContext tracking: ${contextTracking.ssot} SSOT, ${contextTracking.default_pack}.\n\nAuto-sealed ambiguity gate: no user question was required. Continue directly with Team scouting, debate, runtime task graph, implementation, minimum ${MIN_TEAM_REVIEWER_LANES}-lane review, cleanup, reflection, and Honest Mode.\n`);
728
774
  await initTeamLive(id, dir, cleanTask, { agentSessions, roleCounts, roster });
729
775
  const runtime = await writeTeamRuntimeArtifacts(dir, plan, { contractHash });
730
776
  await writeMemorySweepReport(root, dir, { missionId: id }).catch(() => null);
@@ -774,16 +820,19 @@ async function prepareTeam(root, route, task, required, opts = {}) {
774
820
  mission_id: id,
775
821
  task: cleanTask,
776
822
  agent_session_count: agentSessions,
777
- default_agent_session_count: 3,
823
+ default_agent_session_count: MIN_TEAM_REVIEWER_LANES,
778
824
  role_counts: roleCounts,
779
825
  session_policy: `Use at most ${agentSessions} subagent sessions at a time; the parent orchestrator is not counted.`,
826
+ review_policy: teamReviewPolicy(),
827
+ review_gate: evaluateTeamReviewPolicyGate({ roleCounts, agentSessions, roster }),
780
828
  bundle_size: roster.bundle_size,
781
829
  roster,
782
830
  team_model: {
783
831
  phases: ['parallel_analysis_scouts', 'triwiki_stage_refresh', 'debate_team', 'triwiki_stage_refresh', 'runtime_task_graph', 'development_team', 'triwiki_stage_refresh', 'review'],
784
832
  analysis_team: `Read-only parallel scouting with exactly ${roster.bundle_size} analysis_scout_N agents. Each scout owns one investigation slice and returns TriWiki-ready findings with source paths, risks, and suggested implementation slices.`,
785
833
  debate_team: `Read-only role debate with exactly ${roster.bundle_size} participants composed from user, planner, reviewer, and executor voices applying compact Hyperplan-derived adversarial lenses.`,
786
- development_team: `Fresh parallel development bundle with exactly ${roster.bundle_size} executor_N developers implementing disjoint slices; validation_team reviews afterward.`
834
+ development_team: `Fresh parallel development bundle with exactly ${roster.bundle_size} executor_N developers implementing disjoint slices; validation_team reviews afterward.`,
835
+ review_team: `Validation runs at least ${MIN_TEAM_REVIEWER_LANES} independent reviewer/QA lanes before integration or final.`
787
836
  },
788
837
  context_tracking: triwikiContextTracking(),
789
838
  team_runtime: teamRuntimePlanMetadata(),
@@ -795,7 +844,7 @@ async function prepareTeam(root, route, task, required, opts = {}) {
795
844
  { id: 'consensus', goal: `Seal one objective with acceptance criteria and disjoint implementation slices, then refresh/validate TriWiki so implementation receives current consensus context.` },
796
845
  { id: 'runtime_task_graph_compile', goal: `Compile the agreed Team plan into ${TEAM_GRAPH_ARTIFACT}, ${TEAM_RUNTIME_TASKS_ARTIFACT}, and ${TEAM_DECOMPOSITION_ARTIFACT}; remap symbolic plan nodes to concrete task ids, allocate role/path/domain worker lanes, and write ${TEAM_INBOX_DIR} before executor work starts.`, agents: ['parent_orchestrator'], output: [TEAM_GRAPH_ARTIFACT, TEAM_RUNTIME_TASKS_ARTIFACT, TEAM_DECOMPOSITION_ARTIFACT, TEAM_INBOX_DIR] },
797
846
  { id: 'parallel_implementation', goal: `Before implementation, read relevant TriWiki context and current source. Close debate agents, then spawn a fresh ${roster.bundle_size}-person executor development team with non-overlapping write ownership. Refresh TriWiki after implementation changes or blockers.`, agents: roster.development_team.map((agent) => agent.id) },
798
- { id: 'review_integration', goal: 'Before review and final output, read/validate current TriWiki context, integrate executor output, strict review correctness/DB safety/tests, validate user friction with validation_team, refresh after review findings, and record evidence.', agents: roster.validation_team.map((agent) => agent.id) },
847
+ { id: 'review_integration', goal: `Before review and final output, read/validate current TriWiki context, integrate executor output, run at least ${MIN_TEAM_REVIEWER_LANES} independent reviewer/QA validation lanes for correctness/DB safety/tests/evidence, validate user friction with validation_team, refresh after review findings, and record evidence.`, agents: roster.validation_team.map((agent) => agent.id), min_reviewer_lanes: MIN_TEAM_REVIEWER_LANES },
799
848
  { id: 'session_cleanup', goal: `Close or account for all Team subagent sessions, finalize live transcript state, and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection or final.`, agents: ['parent_orchestrator'] }
800
849
  ],
801
850
  live_visibility: {
@@ -810,7 +859,7 @@ async function prepareTeam(root, route, task, required, opts = {}) {
810
859
  await writeJsonAtomic(path.join(dir, 'team-plan.json'), plan);
811
860
  await writeJsonAtomic(path.join(dir, 'team-roster.json'), { schema_version: 1, mission_id: id, role_counts: roleCounts, agent_sessions: agentSessions, bundle_size: roster.bundle_size, roster, confirmed: true, source: 'default_or_prompt_team_spec' });
812
861
  const contextTracking = triwikiContextTracking();
813
- await writeTextAtomic(path.join(dir, 'team-workflow.md'), `# SKS Team Workflow\n\nTask: ${cleanTask}\n\nAgent session budget: ${agentSessions}\nBundle size: ${roster.bundle_size}\nRole counts: ${formatRoleCounts(roleCounts)}\nReasoning: high for team logic, temporary for this route only.\nContext tracking: ${contextTracking.ssot} SSOT, ${contextTracking.default_pack}; use relevant TriWiki context before every work stage, refresh/validate after findings, and preserve hydratable source anchors.\n\n1. Run exactly ${roster.bundle_size} read-only analysis_scout_N agents and write team-analysis.md.\n2. Refresh/validate TriWiki before debate.\n3. Run exactly ${roster.bundle_size} debate participants through the compact Hyperplan-derived adversarial lens pass, then write consensus and implementation slices.\n4. Compile ${TEAM_GRAPH_ARTIFACT}, ${TEAM_RUNTIME_TASKS_ARTIFACT}, ${TEAM_DECOMPOSITION_ARTIFACT}, and ${TEAM_INBOX_DIR} so worker handoff uses concrete runtime task ids.\n5. Close debate agents before starting a fresh ${roster.bundle_size}-person executor team.\n6. Review, integrate, verify, and record evidence.\n7. Close/clean remaining Team sessions, finalize live transcript state, and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection/final.\n\nNo unrequested fallback implementation code is allowed in any stage, executor lane, review lane, MAD route, or MAD-SKS route. If the requested path cannot be implemented inside the sealed contract, block with evidence instead of adding substitute behavior.\n\nLive visibility:\n- sks team log ${id}\n- sks team tail ${id}\n- sks team watch ${id}\n- sks team lane ${id} --agent analysis_scout_1 --follow\n- sks team event ${id} --agent <name> --phase <phase> --message \"...\"\n`);
862
+ await writeTextAtomic(path.join(dir, 'team-workflow.md'), `# SKS Team Workflow\n\nTask: ${cleanTask}\n\nAgent session budget: ${agentSessions}\nBundle size: ${roster.bundle_size}\nRole counts: ${formatRoleCounts(roleCounts)}\nReview policy: ${MIN_TEAM_REVIEW_POLICY_TEXT}\nReasoning: high for team logic, temporary for this route only.\nContext tracking: ${contextTracking.ssot} SSOT, ${contextTracking.default_pack}; use relevant TriWiki context before every work stage, refresh/validate after findings, and preserve hydratable source anchors.\n\n1. Run exactly ${roster.bundle_size} read-only analysis_scout_N agents and write team-analysis.md.\n2. Refresh/validate TriWiki before debate.\n3. Run exactly ${roster.bundle_size} debate participants through the compact Hyperplan-derived adversarial lens pass, then write consensus and implementation slices.\n4. Compile ${TEAM_GRAPH_ARTIFACT}, ${TEAM_RUNTIME_TASKS_ARTIFACT}, ${TEAM_DECOMPOSITION_ARTIFACT}, and ${TEAM_INBOX_DIR} so worker handoff uses concrete runtime task ids.\n5. Close debate agents before starting a fresh ${roster.bundle_size}-person executor team.\n6. Run at least ${MIN_TEAM_REVIEWER_LANES} independent reviewer/QA validation lanes, integrate, verify, and record evidence.\n7. Close/clean remaining Team sessions, finalize live transcript state, and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection/final.\n\nNo unrequested fallback implementation code is allowed in any stage, executor lane, review lane, MAD route, or MAD-SKS route. If the requested path cannot be implemented inside the sealed contract, block with evidence instead of adding substitute behavior.\n\nLive visibility:\n- sks team log ${id}\n- sks team tail ${id}\n- sks team watch ${id}\n- sks team lane ${id} --agent analysis_scout_1 --follow\n- sks team event ${id} --agent <name> --phase <phase> --message \"...\"\n`);
814
863
  await initTeamLive(id, dir, cleanTask, { agentSessions, roleCounts, roster });
815
864
  const runtime = await writeTeamRuntimeArtifacts(dir, plan, {});
816
865
  await writeMemorySweepReport(root, dir, { missionId: id }).catch(() => null);
@@ -823,7 +872,7 @@ async function prepareTeam(root, route, task, required, opts = {}) {
823
872
  : {};
824
873
  const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task: cleanTask, required, ambiguity: { required: false, status: 'direct_team_cli' } });
825
874
  await setCurrent(root, routeState(id, route, 'TEAM_PARALLEL_ANALYSIS_SCOUTING', required, { prompt: cleanTask, implementation_allowed: true, ambiguity_gate_required: false, ambiguity_gate_passed: true, agent_sessions: agentSessions, role_counts: roleCounts, team_roster_confirmed: true, team_plan_ready: true, team_graph_ready: runtime.ok, context_tracking: 'triwiki', from_chat_img_required: fromChatImgRequired, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT, ...madSksState }));
826
- return routeContext(route, id, cleanTask, required, `Run scouts, refresh/validate TriWiki, debate, close debate agents, form a fresh ${roster.bundle_size}-person executor team, then close/clean Team sessions and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection.`);
875
+ return routeContext(route, id, cleanTask, required, `Run scouts, refresh/validate TriWiki, debate, close debate agents, form a fresh ${roster.bundle_size}-person executor team, run minimum ${MIN_TEAM_REVIEWER_LANES}-lane review/integration, then close/clean Team sessions and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection.`);
827
876
  }
828
877
 
829
878
  async function prepareResearch(root, route, task, required) {
@@ -1362,11 +1411,15 @@ function missingRequiredGateFields(file, state, gate = {}) {
1362
1411
  return gate[key] !== true;
1363
1412
  });
1364
1413
  }
1414
+ if (file === IMAGE_UX_REVIEW_GATE_ARTIFACT || mode === 'IMAGE_UX_REVIEW') {
1415
+ return IMAGE_UX_REVIEW_REQUIRED_GATE_FIELDS.filter((key) => gate[key] !== true);
1416
+ }
1365
1417
  return [];
1366
1418
  }
1367
1419
 
1368
1420
  async function missingRequiredGateArtifacts(root, file, state, gate = {}) {
1369
1421
  const mode = String(state?.mode || '').toUpperCase();
1422
+ if (file === IMAGE_UX_REVIEW_GATE_ARTIFACT || mode === 'IMAGE_UX_REVIEW') return missingImageUxReviewArtifacts(root, state, gate);
1370
1423
  if (file !== 'team-gate.json' && mode !== 'TEAM') return [];
1371
1424
  const missing = [];
1372
1425
  if (gate.team_roster_confirmed === true && !(await exists(path.join(missionDir(root, state.mission_id), 'team-roster.json')))) missing.push('team-roster.json');
@@ -1386,6 +1439,42 @@ async function missingRequiredGateArtifacts(root, file, state, gate = {}) {
1386
1439
  return missing;
1387
1440
  }
1388
1441
 
1442
+ async function missingImageUxReviewArtifacts(root, state = {}, gate = {}) {
1443
+ const missing = [];
1444
+ const id = state?.mission_id;
1445
+ if (!id) return [`${IMAGE_UX_REVIEW_GATE_ARTIFACT}:mission_id`];
1446
+ const dir = missionDir(root, id);
1447
+ const required = [
1448
+ [IMAGE_UX_REVIEW_POLICY_ARTIFACT, 'policy_created'],
1449
+ [IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT, 'screen_inventory_created'],
1450
+ [IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT, 'imagegen_review_images_generated'],
1451
+ [IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT, 'issue_ledger_created'],
1452
+ [IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT, 'bounded_iteration_complete']
1453
+ ];
1454
+ for (const [artifact, field] of required) {
1455
+ if (gate[field] === true && !(await exists(path.join(dir, artifact)))) missing.push(artifact);
1456
+ }
1457
+ const generated = await readJson(path.join(dir, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT), null);
1458
+ if (gate.imagegen_review_images_generated === true) {
1459
+ if (!generated) missing.push(IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT);
1460
+ else {
1461
+ if (generated.passed !== true) missing.push(`${IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT}:passed`);
1462
+ if (!Array.isArray(generated.generated_review_images) || generated.generated_review_images.length === 0) missing.push(`${IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT}:generated_review_images`);
1463
+ if (String(generated.provider?.model || '') !== 'gpt-image-2') missing.push(`${IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT}:gpt-image-2`);
1464
+ }
1465
+ }
1466
+ const issues = await readJson(path.join(dir, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT), null);
1467
+ if (gate.generated_review_images_analyzed === true || gate.p0_p1_zero === true) {
1468
+ if (!issues) missing.push(IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT);
1469
+ else {
1470
+ if (issues.passed !== true && gate.p0_p1_zero === true) missing.push(`${IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT}:passed`);
1471
+ if (issues.extraction_source !== IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT) missing.push(`${IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT}:extraction_source`);
1472
+ if (Number(issues.blocking_issue_count || 0) !== 0 && gate.p0_p1_zero === true) missing.push(`${IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT}:blocking_issue_count`);
1473
+ }
1474
+ }
1475
+ return missing;
1476
+ }
1477
+
1389
1478
  function fromChatImgCoverageRequired(state = {}, gate = {}) {
1390
1479
  return state?.from_chat_img_required === true || gate?.from_chat_img_required === true;
1391
1480
  }
@@ -1461,6 +1550,7 @@ function gateFilesForState(state) {
1461
1550
  if (state.mode === 'GX') return ['gx-gate.json'];
1462
1551
  if (state.mode === 'QALOOP') return ['qa-gate.json'];
1463
1552
  if (state.mode === 'PPT') return ['ppt-gate.json'];
1553
+ if (state.mode === 'IMAGE_UX_REVIEW') return [IMAGE_UX_REVIEW_GATE_ARTIFACT];
1464
1554
  return ['done-gate.json'];
1465
1555
  }
1466
1556
 
@@ -7,9 +7,10 @@ export const FROM_CHAT_IMG_CHECKLIST_ARTIFACT = 'from-chat-img-checklist.md';
7
7
  export const FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT = 'from-chat-img-temp-triwiki.json';
8
8
  export const FROM_CHAT_IMG_QA_LOOP_ARTIFACT = 'from-chat-img-qa-loop.json';
9
9
  export const FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS = 5;
10
- export const USAGE_TOPICS = 'install|setup|bootstrap|root|deps|tmux|auto-review|team|qa-loop|ppt|goal|research|db|codex-app|openclaw|dfix|design|imagegen|dollar|context7|pipeline|reasoning|guard|conflicts|versioning|eval|harness|hproof|gx|wiki|code-structure|proof-field|skill-dream';
10
+ export const USAGE_TOPICS = 'install|setup|bootstrap|root|deps|tmux|auto-review|team|qa-loop|ppt|image-ux-review|goal|research|db|codex-app|openclaw|dfix|design|imagegen|dollar|context7|pipeline|reasoning|guard|conflicts|versioning|eval|harness|hproof|gx|wiki|code-structure|proof-field|skill-dream';
11
11
  export const CODEX_COMPUTER_USE_EVIDENCE_SOURCE = 'codex_computer_use';
12
12
  export const CODEX_APP_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com/codex/app/features#image-generation';
13
+ export const OPENAI_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com/api/docs/guides/image-generation';
13
14
  export const CODEX_COMPUTER_USE_ONLY_POLICY = 'Pipeline UI/browser verification and visual inspection must use Codex Computer Use only. Do not use Playwright, Chrome MCP, Browser Use, Selenium, Puppeteer, or any other browser automation substitute; if Codex Computer Use is unavailable, mark the UI/browser evidence unverified instead of substituting another tool.';
14
15
  export const FORBIDDEN_BROWSER_AUTOMATION_RE = /\b(playwright|chrome\s+mcp|browser\s+use|selenium|puppeteer)\b/i;
15
16
 
@@ -95,6 +96,10 @@ export function getdesignReferencePolicyText() {
95
96
  return `Design SSOT policy: ${DESIGN_SYSTEM_SSOT.authority_file} is the single design decision authority. If it is missing, create or update it through ${DESIGN_SYSTEM_SSOT.builder_prompt}; getdesign.md (${GETDESIGN_REFERENCE.url}), its official docs, and curated DESIGN.md examples at ${AWESOME_DESIGN_MD_REFERENCE.url} are source inputs to fuse into that SSOT or into route-local style tokens, not parallel authorities. Prefer the official Codex skill when available (${GETDESIGN_REFERENCE.codex_skill_install}); otherwise use the generated getdesign-reference skill plus official Web/API/CLI/SDK docs and curated DESIGN.md examples as inputs. Do not claim an official getdesign MCP server is configured unless a current official MCP surface is actually available.`;
96
97
  }
97
98
 
99
+ export function imageUxReviewPipelinePolicyText() {
100
+ return `Image UX review pipeline: the core mechanism is not text-only screenshot critique. Capture or receive source UI screenshots, then use Codex App imagegen/$imagegen with gpt-image-2 (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) to create new annotated review images from those screenshots as reference inputs. The generated review image must visibly mark numbered callouts, P0/P1/P2/P3 labels, eye-flow, hierarchy, contrast, alignment, density, affordance problems, and a small corrected mini-comp or before/after strip when useful. Then analyze that generated review image with vision/OCR and convert the visible callouts into image-ux-issue-ledger.json rows. Missing generated review images block image-ux-review-gate.json; never pass this route from a hand-written text-only substitute. For larger API-backed batches, use the official OpenAI image generation/editing path only when OPENAI_API_KEY and the route contract allow API-priced generation (${OPENAI_IMAGE_GENERATION_DOC_URL}).`;
101
+ }
102
+
98
103
  export const RECOMMENDED_SKILLS = [
99
104
  'reasoning-router',
100
105
  'pipeline-runner',
@@ -107,6 +112,7 @@ export const RECOMMENDED_SKILLS = [
107
112
  'design-ui-editor',
108
113
  'getdesign-reference',
109
114
  'imagegen',
115
+ 'image-ux-review',
110
116
  'computer-use',
111
117
  'computer-use-fast',
112
118
  'db-safety-guard',
@@ -270,7 +276,7 @@ export const ROUTES = [
270
276
  context7Policy: 'optional',
271
277
  reasoningPolicy: 'high',
272
278
  stopGate: 'team-gate.json',
273
- cliEntrypoint: 'sks team "task" [executor:5 reviewer:2 user:1] | sks team log|tail|watch|lane|status|event|message|cleanup-tmux',
279
+ cliEntrypoint: 'sks team "task" [executor:5 reviewer:6 user:1] | sks team log|tail|watch|lane|status|event|message|cleanup-tmux',
274
280
  examples: ['$Team executor:5 agree on the best plan and implement it', '$From-Chat-IMG 채팅+첨부 이미지 작업 지시서']
275
281
  },
276
282
  {
@@ -301,6 +307,22 @@ export const ROUTES = [
301
307
  cliEntrypoint: 'Codex App prompt route only: $PPT <topic>',
302
308
  examples: ['$PPT 우리 SaaS 소개자료를 HTML 기반 PDF로 만들어줘', '$PPT 투자자용 피치덱 만들어줘']
303
309
  },
310
+ {
311
+ id: 'ImageUXReview',
312
+ command: '$Image-UX-Review',
313
+ mode: 'IMAGE_UX_REVIEW',
314
+ route: 'image-generation UI/UX review loop',
315
+ description: 'Review UI/UX through the imagegen/gpt-image-2 visual critique loop: source screenshots become generated annotated review images, those images become issue ledgers, then fixes are rechecked.',
316
+ requiredSkills: ['image-ux-review', 'imagegen', 'computer-use', 'pipeline-runner', REFLECTION_SKILL_NAME, 'honest-mode'],
317
+ dollarAliases: ['$UX-Review'],
318
+ appSkillAliases: ['ux-review', 'visual-review', 'ui-ux-review'],
319
+ lifecycle: ['target_and_capture_inventory', 'source_screenshots', 'gpt_image_2_annotated_review_image', 'generated_image_text_extraction', 'issue_ledger', 'optional_safe_fixes', 'changed_screen_recheck', 'post_route_reflection', 'honest_mode'],
320
+ context7Policy: 'if_external_docs',
321
+ reasoningPolicy: 'high',
322
+ stopGate: 'image-ux-review-gate.json',
323
+ cliEntrypoint: 'Codex App prompt route: $Image-UX-Review <target>; inspect with sks image-ux-review status latest',
324
+ examples: ['$Image-UX-Review localhost 화면을 이미지 생성 리뷰 루프로 검수해줘', '$UX-Review 이 스크린샷을 gpt-image-2 콜아웃 리뷰로 분석하고 고쳐줘']
325
+ },
304
326
  {
305
327
  id: 'ComputerUse',
306
328
  command: '$Computer-Use',
@@ -466,6 +488,7 @@ export const COMMAND_CATALOG = [
466
488
  { name: 'dfix', usage: 'sks dfix', description: 'Explain $DFix ultralight design/content fix mode.' },
467
489
  { name: 'qa-loop', usage: 'sks qa-loop prepare|answer|run|status ...', description: 'Dogfood UI/API as human proxy with safety gates, safe fixes, rechecks, Codex Computer Use-only UI evidence, report.' },
468
490
  { name: 'ppt', usage: 'sks ppt build|status <mission-id|latest> [--json]', description: 'Build or inspect $PPT HTML/PDF artifacts from a sealed presentation decision contract.' },
491
+ { name: 'image-ux-review', usage: 'sks image-ux-review status <mission-id|latest> [--json]', description: 'Inspect $Image-UX-Review gpt-image-2/imagegen annotated UI/UX review artifacts.' },
469
492
  { name: 'context7', usage: 'sks context7 check|setup|tools|resolve|docs|evidence ...', description: 'Check, configure, and call the local Context7 MCP requirement.' },
470
493
  { name: 'pipeline', usage: 'sks pipeline status|resume|plan|answer ...', description: 'Inspect the active skill-first route, materialized execution plan, ambiguity gates, and completion gates.' },
471
494
  { name: 'guard', usage: 'sks guard check [--json]', description: 'Check SKS harness self-protection lock, fingerprints, and source-repo exception state.' },
@@ -489,7 +512,7 @@ export const COMMAND_CATALOG = [
489
512
  { name: 'validate-artifacts', usage: 'sks validate-artifacts [mission-id|latest] [--json]', description: 'Validate schema-backed mission artifacts for work orders, effort decisions, visual maps, dogfood reports, skills, mistake memory, Team dashboard state, and Honest Mode.' },
490
513
  { name: 'wiki', usage: 'sks wiki coords|pack|refresh|prune|validate ...', description: 'Build, refresh, prune, and validate RGBA/trig LLM Wiki context packs with attention.use_first and attention.hydrate_first for compact recall plus source hydration.' },
491
514
  { name: 'hproof', usage: 'sks hproof check [mission-id|latest]', description: 'Evaluate the H-Proof done gate for a mission.' },
492
- { name: 'team', usage: 'sks team "task" [executor:5 reviewer:2 user:1]|log|tail|watch|lane|status|dashboard|event|message|cleanup-tmux ...', description: 'Create and observe a scout-first Team mission with color-coded tmux lanes, transcript messages, and cleanup-aware follow panes.' },
515
+ { name: 'team', usage: 'sks team "task" [executor:5 reviewer:6 user:1]|log|tail|watch|lane|status|dashboard|event|message|cleanup-tmux ...', description: 'Create and observe a scout-first Team mission with at least five reviewer/QA validation lanes, color-coded tmux lanes, transcript messages, and cleanup-aware follow panes.' },
493
516
  { name: 'reasoning', usage: 'sks reasoning ["prompt"] [--json]', description: 'Show SKS temporary reasoning-effort routing: medium for simple tasks, high for logic, xhigh for research.' },
494
517
  { name: 'gx', usage: 'sks gx init|render|validate|drift|snapshot [name]', description: 'Create and verify deterministic SVG/HTML visual context cartridges.' },
495
518
  { name: 'profile', usage: 'sks profile show|set <model>', description: 'Inspect or set the current SKS model profile metadata.' },
@@ -554,6 +577,15 @@ export function looksLikePresentationArtifactRequest(prompt = '') {
554
577
  return /만들|작성|생성|제작|디자인|export|pdf|html|create|generate|build|write|make/i.test(text) || /\b(ppt|presentation|deck|slides?)\b/.test(lower);
555
578
  }
556
579
 
580
+ export function looksLikeImageUxReviewRequest(prompt = '') {
581
+ const text = String(prompt || '');
582
+ const reviewCue = /(ui\/?ux|ux|ui|screen|screenshot|visual|interface|화면|스크린|캡처|비주얼|인터페이스|사용성|유아이|유엑스)/i.test(text)
583
+ && /(review|critique|audit|inspect|analy[sz]e|검수|리뷰|분석|평가|진단)/i.test(text);
584
+ const imagegenCue = /(gpt-image-2|imagegen|\$imagegen|image\s*generation|generated\s*review|annotated\s*review|callout|이미지\s*생성|생성\s*이미지|콜아웃|주석\s*이미지)/i.test(text);
585
+ const commandCue = /\$?(?:image-ux-review|ux-review|visual-review|ui-ux-review)\b/i.test(text);
586
+ return commandCue || (reviewCue && imagegenCue);
587
+ }
588
+
557
589
  export function routePrompt(prompt) {
558
590
  const command = dollarCommand(prompt);
559
591
  const text = String(prompt || '');
@@ -574,6 +606,7 @@ export function routePrompt(prompt) {
574
606
  }
575
607
  if (hasFromChatImgSignal(text)) return routeById('Team');
576
608
  if (looksLikePresentationArtifactRequest(text)) return routeById('PPT');
609
+ if (looksLikeImageUxReviewRequest(text)) return routeById('ImageUXReview');
577
610
  if (looksLikeComputerUseFastLane(text)) return routeById('ComputerUse');
578
611
  if (looksLikeFastDesignFix(text)) return routeById('DFix');
579
612
  if (looksLikeQuestionShapedDirective(text)) return routeById('Team');
@@ -644,6 +677,7 @@ export function routeRequiresSubagents(route, prompt = '') {
644
677
  if (route.id === 'SKS') return looksLikeTeamDefaultWork(prompt);
645
678
  if (route.id === 'Help' || route.id === 'Answer' || route.id === 'Wiki' || route.id === 'ComputerUse') return false;
646
679
  if (route.id === 'PPT') return false;
680
+ if (route.id === 'ImageUXReview') return false;
647
681
  if (route.id === 'Research' || route.id === 'AutoResearch') return true;
648
682
  if (route.id === 'Goal') return looksLikeExecutionWork(prompt) || looksLikeTeamDefaultWork(stripDollarCommand(prompt));
649
683
  if (route.id === 'DB' || route.id === 'GX') return looksLikeExecutionWork(prompt);
@@ -653,7 +687,7 @@ export function routeRequiresSubagents(route, prompt = '') {
653
687
 
654
688
  export function reflectionRequiredForRoute(route) {
655
689
  const id = String(route?.id || route?.mode || route?.route || route || '').replace(/^\$/, '');
656
- return /^(team|qaloop|qa-loop|ppt|research|autoresearch|db|database|madsks|mad-sks|gx)$/i.test(id);
690
+ return /^(team|qaloop|qa-loop|ppt|imageuxreview|image-ux-review|research|autoresearch|db|database|madsks|mad-sks|gx)$/i.test(id);
657
691
  }
658
692
 
659
693
  export function looksLikeCodeChangingWork(prompt = '') {
@@ -696,6 +730,7 @@ export function routeReasoning(route, prompt = '') {
696
730
  const base = ALLOWED_REASONING_EFFORTS.has(route?.reasoningPolicy) ? route.reasoningPolicy : 'medium';
697
731
  if (hasFromChatImgSignal(text)) return reasoning('xhigh', 'from_chat_img_image_work_order_analysis');
698
732
  if (route?.id === 'Research' || route?.id === 'AutoResearch') return reasoning('xhigh', 'research_or_experiment_route');
733
+ if (route?.id === 'ImageUXReview') return reasoning('high', 'image_generation_visual_review_route');
699
734
  if (/\b(research|autoresearch|hypothesis|falsify|novelty|frontier|benchmark|experiment|SEO|GEO|ranking|연구|실험|가설|검증)\b/i.test(text)) return reasoning('xhigh', 'research_level_prompt');
700
735
  if (base === 'xhigh') return reasoning('xhigh', 'route_policy_xhigh');
701
736
  if (base === 'high' || /\b(architecture|design|migration|database|security|parallel|orchestrat|refactor|algorithm|logic|tradeoff|검토|설계|마이그레이션|보안|병렬|팀|논리)\b/i.test(text)) return reasoning('high', 'logical_or_safety_work');
@@ -1,11 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import { appendJsonlBounded, nowIso, readJson, readText, writeJsonAtomic, writeTextAtomic } from './fsx.mjs';
3
3
  import { triwikiContextTracking, triwikiContextTrackingText } from './routes.mjs';
4
+ import { MIN_TEAM_REVIEWER_LANES, MIN_TEAM_REVIEW_STAGE_AGENT_SESSIONS } from './team-review-policy.mjs';
5
+ export { MIN_TEAM_REVIEWER_LANES, MIN_TEAM_REVIEW_POLICY_TEXT, MIN_TEAM_REVIEW_STAGE_AGENT_SESSIONS, evaluateTeamReviewPolicyGate, teamReviewPolicy } from './team-review-policy.mjs';
4
6
 
5
7
  const MAX_LIVE_BYTES = 192 * 1024;
6
8
  const TEAM_RUNTIME_TASKS_ARTIFACT = 'team-runtime-tasks.json';
7
9
  const DEFAULT_AGENTS = ['parent_orchestrator', 'analysis_scout', 'team_consensus', 'implementation_worker', 'db_safety_reviewer', 'qa_reviewer'];
8
- export const DEFAULT_TEAM_ROLE_COUNTS = { user: 1, planner: 1, reviewer: 1, executor: 3 };
10
+ export const DEFAULT_TEAM_ROLE_COUNTS = { user: 1, planner: 1, reviewer: MIN_TEAM_REVIEWER_LANES, executor: 3 };
9
11
  export const DEFAULT_MAX_TEAM_AGENT_SESSIONS = 6;
10
12
  const ROLE_ALIASES = {
11
13
  user: 'user',
@@ -107,6 +109,7 @@ ${prompt}
107
109
  - Debate uses compact Hyperplan-derived adversarial lenses: challenge framing, subtract surface, demand evidence, test integration risk, and consider one simpler alternative.
108
110
  - User personas are intentionally impatient, self-interested, stubborn, low-context, and dislike inconvenience.
109
111
  - Executors are capable developers with disjoint ownership.
112
+ - Team reviewer lane policy enforces at least ${MIN_TEAM_REVIEWER_LANES} strict reviewers and enough review-stage parallel capacity.
110
113
  - Reviewers are strict and adversarial about correctness, safety, tests, and evidence.
111
114
  - Every useful subagent status, debate result, handoff, review finding, and integration decision must be appended here.
112
115
  - Before reflection/final, close or account for all Team subagent sessions and write team-session-cleanup.json.
@@ -247,7 +250,8 @@ export function normalizeTeamSpec(opts = {}) {
247
250
  roleCounts.executor = normalizeTeamAgentSessions(opts.agentSessions, roleCounts.executor);
248
251
  }
249
252
  const bundleSize = normalizeTeamAgentSessions(roleCounts.executor, DEFAULT_TEAM_ROLE_COUNTS.executor);
250
- const agentSessions = normalizeTeamAgentSessions(opts.agentSessions ?? bundleSize);
253
+ const reviewStageSessions = normalizeTeamAgentSessions(roleCounts.reviewer, MIN_TEAM_REVIEW_STAGE_AGENT_SESSIONS);
254
+ const agentSessions = Math.max(normalizeTeamAgentSessions(opts.agentSessions ?? bundleSize), reviewStageSessions);
251
255
  return { agentSessions, bundleSize, roleCounts, roster: buildTeamRoster(roleCounts) };
252
256
  }
253
257
 
@@ -257,6 +261,7 @@ export function normalizeTeamRoleCounts(input = {}) {
257
261
  const role = normalizeTeamRole(key);
258
262
  if (role) counts[role] = normalizeTeamAgentSessions(value, counts[role] || 1);
259
263
  }
264
+ counts.reviewer = Math.max(MIN_TEAM_REVIEWER_LANES, counts.reviewer);
260
265
  return counts;
261
266
  }
262
267
 
@@ -0,0 +1,49 @@
1
+ export const MIN_TEAM_REVIEWER_LANES = 5;
2
+ export const MIN_TEAM_REVIEW_STAGE_AGENT_SESSIONS = MIN_TEAM_REVIEWER_LANES;
3
+ export const MIN_TEAM_REVIEW_POLICY_TEXT = `Minimum Team review policy: run at least ${MIN_TEAM_REVIEWER_LANES} independent reviewer/QA validation lanes before integration or final, even when the requested reviewer role count is lower.`;
4
+
5
+ function numericCount(value, fallback = 0) {
6
+ const number = Number(value);
7
+ return Number.isFinite(number) ? Math.max(0, Math.floor(number)) : fallback;
8
+ }
9
+
10
+ export function teamReviewPolicy() {
11
+ return {
12
+ gate: 'team_review_policy',
13
+ minimum_reviewer_lanes: MIN_TEAM_REVIEWER_LANES,
14
+ minimum_review_stage_agent_sessions: MIN_TEAM_REVIEW_STAGE_AGENT_SESSIONS,
15
+ text: MIN_TEAM_REVIEW_POLICY_TEXT
16
+ };
17
+ }
18
+
19
+ export function teamValidationReviewerCount(roster = {}) {
20
+ const validation = Array.isArray(roster?.validation_team) ? roster.validation_team : [];
21
+ return validation.filter((agent) => {
22
+ const id = String(agent?.id || agent || '');
23
+ const role = String(agent?.role || '');
24
+ return /review|qa|validation/i.test(`${role} ${id}`);
25
+ }).length;
26
+ }
27
+
28
+ export function evaluateTeamReviewPolicyGate({ roleCounts = {}, agentSessions = 0, roster = {} } = {}) {
29
+ const requestedReviewerLanes = numericCount(roleCounts.reviewer);
30
+ const requiredReviewerLanes = Math.max(MIN_TEAM_REVIEWER_LANES, requestedReviewerLanes);
31
+ const validationReviewerLanes = teamValidationReviewerCount(roster);
32
+ const sessionCount = numericCount(agentSessions);
33
+ const blockers = [];
34
+
35
+ if (requestedReviewerLanes < MIN_TEAM_REVIEWER_LANES) blockers.push('role_counts.reviewer_below_minimum');
36
+ if (validationReviewerLanes < requiredReviewerLanes) blockers.push('validation_team_reviewers_below_required');
37
+ if (sessionCount < requiredReviewerLanes) blockers.push('agent_sessions_below_review_required');
38
+
39
+ return {
40
+ gate: 'team_review_policy',
41
+ passed: blockers.length === 0,
42
+ blockers,
43
+ minimum_reviewer_lanes: MIN_TEAM_REVIEWER_LANES,
44
+ required_reviewer_lanes: requiredReviewerLanes,
45
+ requested_reviewer_lanes: requestedReviewerLanes,
46
+ validation_reviewer_lanes: validationReviewerLanes,
47
+ agent_sessions: sessionCount
48
+ };
49
+ }
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
4
4
  import { exists, nowIso, packageRoot, readJson, runProcess, sha256, sksRoot, which, writeJsonAtomic } from './fsx.mjs';
5
5
  import { getCodexInfo } from './codex-adapter.mjs';
6
6
  import { codexAppIntegrationStatus, formatCodexAppStatus } from './codex-app.mjs';
7
+ import { MIN_TEAM_REVIEWER_LANES } from './team-review-policy.mjs';
7
8
 
8
9
  export const SKS_TMUX_LOGO = [
9
10
  ' _______ __ __ _______',
@@ -483,25 +484,48 @@ function printTmuxLaunchBlocked(plan, opts = {}) {
483
484
  for (const blocker of Array.from(new Set(plan.blockers))) console.log(`- ${blocker}`);
484
485
  }
485
486
 
486
- export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFile = null, json = false } = {}) {
487
- const launch = await buildTmuxLaunchPlan({ root, session: `sks-team-${missionId}` });
488
- const agents = [
489
- ...(plan.roster?.analysis_team || []),
490
- ...(plan.roster?.debate_team || []),
491
- ...(plan.roster?.development_team || []),
492
- ...(plan.roster?.validation_team || [])
493
- ];
494
- const uniqueAgents = [];
487
+ function uniqueAgentIds(agents = []) {
488
+ const ids = [];
495
489
  const seen = new Set();
496
490
  for (const agent of agents) {
497
- const id = agent.id || String(agent);
498
- if (seen.has(id)) continue;
491
+ const id = agent?.id || String(agent || '');
492
+ if (!id || seen.has(id)) continue;
499
493
  seen.add(id);
500
- uniqueAgents.push(id);
494
+ ids.push(id);
501
495
  }
502
- const commands = uniqueAgents.slice(0, Math.max(1, plan.agent_session_count || 3)).map((agentId, index) => ({
496
+ return ids;
497
+ }
498
+
499
+ function teamViewAgentIds(plan = {}) {
500
+ const roster = plan.roster || {};
501
+ const analysis = uniqueAgentIds(roster.analysis_team || []);
502
+ const debate = uniqueAgentIds(roster.debate_team || []);
503
+ const development = uniqueAgentIds(roster.development_team || []);
504
+ const validation = uniqueAgentIds(roster.validation_team || []);
505
+ const reviewers = validation.filter((id) => teamLaneStyle(id).role === 'review');
506
+ const reviewerTarget = Math.max(MIN_TEAM_REVIEWER_LANES, Number(plan.review_policy?.minimum_reviewer_lanes) || 0, Number(plan.role_counts?.reviewer) || 0);
507
+ const reviewLanes = reviewers.slice(0, reviewerTarget);
508
+ const representative = [analysis[0], development[0], debate[0]].filter(Boolean);
509
+ const ordered = [...reviewLanes, ...representative, ...analysis, ...debate, ...development, ...validation];
510
+ const limit = Math.max(Number(plan.agent_session_count) || MIN_TEAM_REVIEWER_LANES, reviewLanes.length + representative.length);
511
+ return uniqueAgentIds(ordered).slice(0, Math.max(1, limit));
512
+ }
513
+
514
+ function teamLanePhase(agentId = '') {
515
+ const role = teamLaneStyle(agentId).role;
516
+ if (role === 'review') return 'review';
517
+ if (role === 'execution') return 'implementation';
518
+ if (role === 'scout') return 'analysis';
519
+ if (role === 'safety') return 'safety';
520
+ return 'team';
521
+ }
522
+
523
+ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFile = null, json = false } = {}) {
524
+ const launch = await buildTmuxLaunchPlan({ root, session: `sks-team-${missionId}` });
525
+ const visibleAgents = teamViewAgentIds(plan);
526
+ const commands = visibleAgents.map((agentId) => ({
503
527
  agent: agentId,
504
- command: teamAgentCommand(launch.root, missionId, agentId, index === 0 ? 'analysis' : 'team', promptFile),
528
+ command: teamAgentCommand(launch.root, missionId, agentId, teamLanePhase(agentId), promptFile),
505
529
  style: teamLaneStyle(agentId),
506
530
  title: teamLaneTitle(agentId)
507
531
  }));